I made IMAP(IMAPS) notifier on boost-ext/sml state machine. There is an opinion that networking is best done on machines. Compile in C++20. Tested on GMail server. Uses karastojko/mailio (and therefore boost+openssl)
Just checking new emails every c_RecheckInterval_minutes
minutes.
Here are two key points that concern me:
- State machine definition in Impl.h inside
struct ImapsSm
; - State machine using in Thread.h inside
void Thread::run()
.
Impl.h
#pragma once
namespace StateMachine {
// for action
typedef std::function< void(const std:string&) > setActiveTip_t;
// events
struct CheckAccess{};
struct Unavailable{};
struct StartLoop{};
struct HasBreak{};
struct RenewEmail{};
struct ContinueLoop{};
// dependencies
struct dependencyImaps {
const Credentials m_credentials;
std::chrono::seconds c_connectTimeout_sec;
};
struct dependencyEmailCount {
setActiveTip_t fnSetActiveTip;
unsigned long m_currentEmailCount, m_lastEmailCount;
std::string lastFrom, lastSubject
};
struct dependencyBreakableSleep {
const std::chrono::seconds c_RecheckInterval_sec;
std::condition_variable *m_pConditionVariable;
std::mutex *m_pcvMutex;
bool m_bBreak;
};
struct dependencyAvailable {
bool m_bAvailable;
};
namespace Action {
struct Imap { void operator()(dependencyImaps &imaps, dependencyEmailCount& counter) {
std::unique_ptr< mailio::imaps > pmailio;
pmailio = std::make_unique< mailio::imaps >(
imaps.m_credentials.host
, imaps.m_credentials.port
, imaps.c_connectTimeout_sec
);
pmailio ->authenticate(
imaps.m_credentials.login
, imaps.m_credentials.password
, mailio::imaps::auth_method_t::LOGIN
);
const char mailbox[] = "inbox";
pmailio ->select( mailbox );
std::list< mailio::imap::search_condition_t > search_condition;
// UNSEEN exclude ALL
search_condition.emplace_back( mailio::imap::search_condition_t::UNSEEN );
std::list<unsigned long> results;
pmailio ->search( search_condition, results );
unsigned long lastSequenceNumber = results.back( );
// Find out sender and subject
mailio::message lastMessage;
bool header_only;
pmailio ->fetch( mailbox, lastSequenceNumber, lastMessage, header_only = true );
counter.m_currentEmailCount = lastSequenceNumber;
counter.lastFrom = lastMessage.from_to_string( );
counter.lastSubject = lastMessage.subject( );
} } Imap;
struct BreakableSleep { void operator()(dependencyBreakableSleep &dependency) {
std::unique_lock< std::mutex > lock( *dependency.m_pcvMutex );
std::cv_status finishedWaiting = dependency.m_pConditionVariable ->wait_until(
lock
, std::chrono::system_clock::now( ) + dependency.c_RecheckInterval_sec
);
if ( std::cv_status::no_timeout == finishedWaiting )
dependency.m_bBreak = true;
} } BreakableSleep;
struct InitialEmailCount { void operator()(dependencyEmailCount& dependency) {
dependency.m_lastEmailCount = dependency.m_currentEmailCount;
} } InitialEmailCount;
struct EmailCounter { void operator()(dependencyEmailCount& dependency) {
if ( dependency.m_currentEmailCount <= dependency.m_lastEmailCount )
return;
dependency.m_lastEmailCount = dependency.m_currentEmailCount;
dependency.fnSetActiveTip( std::string{ }
+ "SmallImapsNotifier: last email\n"
+ "from: '" + dependency.lastFrom + "'\n"
+ "subject: '" + dependency.lastSubject + "'\n"
);
} } EmailCounter;
struct InboxAvailable { void operator()(dependencyAvailable& dependency) {
dependency.m_bAvailable = true;
} } InboxAvailable;
} // namespace Action
struct guardAvailable {
bool operator()(dependencyAvailable& dependency) const {
return !dependency.m_bAvailable;
}
} guardAvailable;
struct guardBreak {
bool operator()(dependencyBreakableSleep &dependency) const {
return dependency.m_bBreak;
}
} guardBreak;
struct ImapsSm {
auto operator()() const noexcept {
using namespace boost::sml;
using namespace Action;
return make_transition_table(
// Using `state<>` to avoid print anonymous on sml logging in MSVC
*state<struct idle> + event<CheckAccess> / Imap = "TryInbox"_s
, "TryInbox"_s + event<Unavailable> [guardAvailable] = X
, "TryInbox"_s + event<StartLoop> / InitialEmailCount = "Waiting"_s
, "TryInbox"_s + on_entry<_> / InboxAvailable
, "Waiting"_s + event<HasBreak> [guardBreak] = X
, "Waiting"_s + event<RenewEmail> / Imap= "RenewedEmail"_s
, "Waiting"_s + on_entry<_> / BreakableSleep
, "RenewedEmail"_s + event<Unavailable> [guardAvailable] = X
, "RenewedEmail"_s + event<ContinueLoop> = "Waiting"_s
, "RenewedEmail"_s + on_entry<_> / ( InboxAvailable, EmailCounter )
, *("exceptions handling"_s) + exception<_> / [](dependencyAvailable& dependency) { dependency.m_bAvailable = false; } = X
);
}
};
} // namespace StateMachine
Thread.h
#pragma once
class Thread {
static constexpr std::chrono::seconds c_ReconnectTry_sec{ 60 };
static constexpr std::chrono::seconds c_ConnectTimeout_sec{ 30 };
static constexpr std::chrono::minutes c_RecheckInterval_minutes{ 5 };
Credentials m_credentials;
StateMachine::setActiveTip_t m_clbSetActiveTip;
std::condition_variable m_cv;
std::mutex m_cvMutex;
std::atomic_bool m_bStop;
std::thread m_thread;
bool breakableWaitReconnect_() {
if ( m_bStop )
return false;
std::unique_lock< std::mutex > lock( m_cvMutex );
std::cv_status waiting = m_cv.wait_until( lock,
std::chrono::system_clock::now( ) + c_ReconnectTry_sec );
return std::cv_status::timeout == waiting;
}
// thread
void run() {
namespace sml = boost::sml;
using namespace StateMachine;
do {
dependencyImaps imaps = { m_credentials, c_ConnectTimeout_sec };
dependencyEmailCount counter = { m_clbSetActiveTip };
dependencyBreakableSleep sleep = {
c_RecheckInterval_minutes
, &m_cv
, &m_cvMutex
};
sml::sm< ImapsSm > sm{ imaps, counter, sleep };
sm.process_event( CheckAccess{ } );
sm.process_event( Unavailable{ } );
sm.process_event( StartLoop{ } );
while ( !sm.is( sml::X ) ) {
sm.process_event( HasBreak{ } );
sm.process_event( RenewEmail{ } );
sm.process_event( Unavailable{ } );
sm.process_event( ContinueLoop{ } );
}
} while ( breakableWaitReconnect_( ) );
}
public:
Thread(
const Credentials &credentials
, StateMachine::setActiveTip_t clbSetActiveTip
)
: m_credentials( credentials )
, m_clbSetActiveTip( clbSetActiveTip )
, m_bStop( false )
{
// will be init in last order
m_thread = { &Thread::run, this };
}
void stop() {
m_bStop = true;
m_cv.notify_all( );
m_thread.join( );
}
};
demo main.cpp, with invalid login and password
#include "pch.h"
#include <mailio/imap.hpp>
#include <boost/sml.hpp>
struct Credentials {
std::string host;
uint16_t port;
std::string login, password;
};
#include "Impl.h"
#include "Thread.h"
void main() {
Credentials credentials = {
"imap.gmail.com", 993, "%login%", "%password%"
};
auto imapsThread = std::make_unique< Thread >(
credentials
, [](const std::string&str) {
std::cout << str;
}
);
getchar( );
}
How does all this correspond to the "canons" of state machines? Not overkill?
Full code on GitHub repo
Thanks!