Mailer 1.0.5
C++ daemon for sending order emails via SMTP
Loading...
Searching...
No Matches
mailer.cpp
Go to the documentation of this file.
1#include <constants.h>
2#include <email_sender.h>
3#include <ini.h>
4#include <logger.h>
5#include <products.h>
6
7#include <atomic>
8#include <csignal>
9#include <cstdlib>
10#include <iostream>
11#include <limits.h>
12#include <pqxx/pqxx>
13#include <setjmp.h>
14#include <string>
15#include <unistd.h>
16#include <unordered_set>
17
18#define SUCCESS (0)
19#define FAILURE (-1)
20
21/// Глобальный флаг последнего полученного сигнала.
22volatile std::sig_atomic_t gSignalStatus;
23
24/// jmp_buf для graceful shutdown (SIGINT/SIGTERM).
26/// jmp_buf для перезапуска цикла после SIGSEGV.
28
29/// Обработчик сигналов завершения (SIGINT/SIGTERM/SIGABRT/SIGFPE/SIGILL).
30static void sigIntHandler(int signal);
31
32/// Обработчик segmentation fault — перезапускает цикл.
33static void sigsegvhandler(int signal);
34
35/// Возвращает директорию, в которой находится исполняемый файл.
36static std::string getApplicationDirectory();
37
38/// Главный класс демона.
39///
40/// Управляет основным циклом: читает INI-конфиг, подключается к PostgreSQL,
41/// проверяет таблицы заказов и отправляет email через SMTP.
42class Daemon {
43public:
44 /// Конструктор.
45 ///
46 /// Инициализирует логгер, читает INI-файл (или генерирует шаблон),
47 /// регистрирует старт в таблице products_log.
48 Daemon() : running(true) {
49 std::string appPath = getApplicationDirectory();
50 Logger::Log();
51 Logger::addOutput(
52 (std::wstring(appPath.begin(), appPath.end()) + L"/mailer.log")
53 .c_str());
54 Logger::addOutput(std::wcout);
55 Logger::setLevel(Logger::L_TRACE);
56 Logger::setLevelFromEnv();
57
58 LOGI(this->name.c_str() << ", version: " << this->version.c_str());
59
60 LOGI("Logger initialized");
61 LOGI((std::string("Executable path: ") + appPath).c_str());
62
63 LOGT("Daemon constructor");
64
65 LOGT("Initializing INI file handler");
66 std::string iniFileName = appPath + "/mailer.ini";
67 LOGI("INI path: " << iniFileName.c_str());
68 iniFile = new mINI::INIFile(iniFileName);
69
70 if (!iniFile->read(ini)) {
71 mINI::INIStructure generatedIni;
72 generatedIni["Mailer"].set({{{"sleepPeriod", "30"}}});
73 generatedIni["MailSender"].set(
74 {{"server", "mail.gentoos.ru"},
75 {"port", "465"},
76 {"protocol", "smtps"},
77 {"sender", "e.anisimov@gentoos.ru"},
78 {"password", "secret"},
79 {"senderName", "Eugene Anisimov"},
80 {"ordersMail", "orders@techno-liga74.ru"}});
81 generatedIni["Postgres"].set({{"user", "K"},
82 {"password", "secret"},
83 {"host", "localhost"},
84 {"port", "5432"},
85 {"dbname", "K"},
86 {"target_session_attrs", "read-write"}});
87 if (iniFile->generate(generatedIni, false)) {
88 LOGI("Failed to read INI file " << iniFileName.c_str()
89 << ", example generated");
90 } else
91 LOGE("Failed to read INI file " << iniFileName.c_str());
92 }
93
94 try {
95 LOGI("Registering program start");
96 productsLog = new Products::ProductsLog(this->name, this->version);
97 productsLog->postMessage("Service started");
98 } catch (const std::exception &e) {
99 LOGE("Failed to register program start" << std::endl << e.what());
100 }
101 };
102
103 /// Деструктор. Освобождает INI-файл, логгирует остановку, отключается от PG.
105 LOGT("Daemon destructor");
106
107 LOGT("Disposing INI");
108 delete iniFile;
109
110 try {
111 LOGI("Registering program stop");
112 productsLog->postMessage("Service stopped");
113 delete productsLog;
114 } catch (const std::exception &e) {
115 LOGE("Failed to register program start" << std::endl << e.what());
116 }
117
118 LOGT("Disconnecting Postgres");
119 DisconnectPg();
120 LOGT("Deinitializing EmailSender");
121 }
122
123 /// Останавливает основной цикл демона.
124 void Stop() { running = false; }
125
126 /// Основной цикл демона.
127 ///
128 /// На каждой итерации:
129 /// - перечитывает INI-файл (конфиг обновляется на лету);
130 /// - подключается к PostgreSQL;
131 /// - проверяет tab_siteOrders и tab_mobileOrders на новые заказы;
132 /// - отправляет email по каждому заказу;
133 /// - переходит в сон на sleepPeriod секунд.
134 void Run() {
135 int status = SUCCESS;
136
137 do {
138 // Read INI file every time
139 if (!iniFile->read(ini)) {
140 LOGE("Failed to read INI file");
141 daemonSleep();
142 continue;
143 }
144
145 try {
146 ConnectPg();
147 } catch (const std::exception &e) {
148 LOGE(e.what());
149 daemonSleep();
150 continue;
151 };
152
153 status = sendOrders("tab_siteOrders");
154 if (status != SUCCESS)
155 LOGE("Error executing checkForSiteOrders");
156 status = sendOrders("tab_mobileOrders");
157 if (status != SUCCESS)
158 LOGE("Error executing checkForSiteOrders");
159
160 if (setjmp(return_to_bottom)) {
161 LOGD("Bottom from sigsegv ");
162 LOGD("Trying to reincarnate");
163 }
164
165 daemonSleep();
166 } while (running);
167 }
168
169private:
170#ifdef NAME
171 std::string name = NAME; ///< Имя сервиса (по умолчанию "Mailer").
172#else
173 std::string name = "Mailer";
174#endif
175#ifdef VERSION
176 std::string version = VERSION; ///< Версия (из CMake compile_definition).
177#else
178 std::string version = "Dev";
179#endif
180
181 mINI::INIFile *iniFile; ///< INI-файл конфигурации.
182 mINI::INIStructure ini; ///< Структура INI-данных в памяти.
183
184 std::string connectionStr = ""; ///< Строка подключения к PostgreSQL.
185 pqxx::connection *connection = NULL; ///< Соединение с PostgreSQL.
186 pqxx::work *trx = NULL; ///< Текущая транзакция.
187
188 Products::ProductsLog *productsLog = NULL; ///< Логирование старта/стопа в БД.
189
190 volatile std::atomic<bool> running = false; ///< Флаг работы демона.
191 int sleepPeriod = 60; ///< Пауза между итерациями (сек).
192
193 /// Проверяет таблицу заказов и отправляет email по каждому неотправленному.
194 ///
195 /// SELECT unsent записей из tableName, группирует по ключу, формирует
196 /// HTML-письмо с таблицей товаров, отправляет через EmailSender.
197 /// При успехе отмечает sent=true.
198 /// @param[in] tableName Имя таблицы ("tab_siteOrders" или
199 /// "tab_mobileOrders").
200 /// @return SUCCESS при успехе, FAILURE при ошибке SQL.
201 int sendOrders(std::string tableName) {
202 int status = SUCCESS;
203
204 LOGI("Checking table " << tableName.c_str() << " for new orders");
205
207 pqxx::result res;
208 try {
209 res = trx->exec("select key, \"N_Cat\", \"Name\", price, count, sent, "
210 "phone, email from " +
211 tableName + " where sent=false");
212 } catch (const std::exception &e) {
213 LOGE("Failed to select from " << tableName.c_str());
214 LOGE(e.what());
215 return FAILURE;
216 }
218
219 std::unordered_set<std::string> keys;
220 for (auto const &row : res) {
221 // If we already handled this key, continue
222 if (keys.contains(std::string(row["key"].c_str())))
223 continue;
224
225 LOGD("Sending mail to " << row["email"].c_str()
226 << " with ID: " << row["key"].c_str());
228 LOGD("Sending email with ID: " << row["key"].c_str() << " for "
229 << row["email"].c_str());
230
231 EmailSender email_sender(
232 ini["EmailSender"]["server"],
233 std::atoi(ini["EmailSender"]["port"].c_str()),
234 ini["EmailSender"]["protocol"], ini["EmailSender"]["sender"],
235 ini["EmailSender"]["password"], ini["EmailSender"]["senderName"]);
236 email_sender.addRecvEmailAddr(ini["EmailSender"]["ordersMail"], "Orders");
237 email_sender.addCcEmailAddr(row["email"].c_str(), row["phone"].c_str());
238
239 // Generate e-mail body
240 std::string subject =
241 std::string("Заявка ") + row["key"].c_str() +
242 (tableName == "tab_siteOrders" ? " с сайта"
243 : " из мобильного приложения");
244
245 std::string body =
246 std::string("e-mail: <b>") + row["email"].c_str() +
247 "</b><br>\nphone: <b>" + row["phone"].c_str() +
248 "</b><br><br>\n"
249 "<table border=\"1\">\n"
250 "<tr><td>Кат. "
251 "номер</td><td>Кол-во</td><td>Цена</td><td>Наименование</td></tr>\n";
252 for (auto const &row_inner : res) {
253 if (row_inner["key"] == row["key"])
254 body += std::string("<tr>\n<td>") + row_inner["\"N_Cat\""].c_str() +
255 "</td>\n<td>" + row_inner["count"].c_str() + "</td>\n<td>" +
256 row_inner["price"].c_str() + "</td>\n<td>" +
257 row_inner["\"Name\""].c_str() + "</td>\n</tr>\n";
258 }
259 body += "</table>";
260 // Set new subject and body
261 email_sender.setEmailContent(subject, body);
262
263 status = email_sender.send();
264
265 if (status == SUCCESS) {
266 LOGD("Updating email status for ID: " << row["key"].c_str());
267 trx->exec_params("update " + tableName + " set sent=true where key=$1",
268 row["key"].c_str());
270 } else {
271 LOGD("Failed to send mail for ID: " << row["key"].c_str());
273 }
274
275 keys.insert(row["key"].c_str());
276 }
277
278 return status;
279 };
280
281 /// Подключается к PostgreSQL.
282 ///
283 /// Формирует строку подключения из секции [Postgres] INI-файла.
284 /// Если подключение уже установлено и параметры не изменились — пропускает.
285 /// @throw std::runtime_error при ошибке подключения.
286 void ConnectPg() {
287 std::string connStr =
288 "user=" + ini["Postgres"]["user"] + " " +
289 "password=" + ini["Postgres"]["password"] + " " +
290 "host=" + ini["Postgres"]["host"] + " " +
291 "port=" + ini["Postgres"]["port"] + " " +
292 "dbname=" + ini["Postgres"]["dbname"] + " " +
293 "target_session_attrs=" + ini["Postgres"]["target_session_attrs"];
294 if (connection && (connStr == connectionStr)) {
295 LOGT("Not reconnecting to PG, connection already established, "
296 "connectionStr isn't changed");
297 return;
298 }
299
300 connectionStr = connStr;
301 try {
302 // Connect to the database
303 LOGI("Connecting to Postgresql");
304 connection = new pqxx::connection(connectionStr);
305 } catch (const std::exception &e) {
306 throw std::runtime_error(e.what());
307 }
308 };
309
310 /// Отключается от PostgreSQL.
312 LOGI("Disconnecting Postgresql");
313
314 if (connection && connection->is_open())
315 connection->close();
316 delete connection;
317 }
318
319 /// Создаёт новую транзакцию PostgreSQL.
321 LOGT("Starting transaction");
322 trx = new pqxx::work(*connection);
323 };
324
325 /// Откатывает текущую транзакцию.
327 LOGT("Aborting transaction");
328 trx->abort();
329 delete trx;
330 trx = NULL;
331 }
332
333 /// Фиксирует текущую транзакцию.
335 LOGT("Committing transaction");
336 trx->commit();
337 delete trx;
338 trx = NULL;
339 }
340
341 /// Пауза между итерациями цикла.
342 ///
343 /// sleepPeriod читается из секции [Mailer] INI-файла на каждом вызове,
344 /// поэтому изменение конфига применяется без перезапуска.
345 void daemonSleep() {
346 sleepPeriod = std::atoi(ini["Mailer"]["sleepPeriod"].c_str());
347 if (sleepPeriod == 0)
348 sleepPeriod = 30;
349 LOGI("Sleeping for " << sleepPeriod << " seconds");
350 sleep(sleepPeriod);
351 }
352};
353
354/// Глобальный указатель на демон (доступен из signal handler'ов).
355static Daemon *d;
356
357/// Точка входа.
358///
359/// Устанавливает обработчики сигналов, создаёт Daemon, запускает цикл Run().
360/// При SIGINT/SIGTERM/SIGABRT/SIGFPE/SIGILL происходит longjmp в эту же
361/// функцию для graceful shutdown (Stop() + delete).
362/// @return 0 при штатном завершении.
363int main(void) {
364 std::signal(SIGSEGV, &sigsegvhandler);
365 std::signal(SIGINT, &sigIntHandler);
366 std::signal(SIGABRT, &sigIntHandler);
367 std::signal(SIGFPE, &sigIntHandler);
368 std::signal(SIGILL, &sigIntHandler);
369 std::signal(SIGTERM, &sigIntHandler);
370
371 try {
372 d = new Daemon();
373 } catch (const std::exception &e) {
374 LOGE(e.what());
375 }
376
377 if (setjmp(return_to_main)) {
378 d->Stop();
379 delete d;
380 } else {
381 try {
382 d->Run();
383 } catch (const std::exception &e) {
384 LOGE(e.what());
385 }
386 }
387
388 return 0;
389}
390
391/// Обработчик сигналов завершения.
392///
393/// Выполняет longjmp в main() для штатной остановки демона.
394/// @param[in] signal Номер сигнала (SIGINT/SIGTERM/SIGABRT/SIGFPE/SIGILL).
395static void sigIntHandler(int signal) {
396 LOGD("Turning off daemon");
397 LOGD(" Signal was " + signal);
398 longjmp(return_to_main, 1);
399}
400
401/// Обработчик segmentation fault.
402///
403/// Выполняет longjmp в точку return_to_bottom внутри цикла Run(),
404/// позволяя демону продолжить работу без полного перезапуска.
405/// @param[in] signal Номер сигнала (SIGSEGV).
406static void sigsegvhandler(int signal) {
407 LOGD("Try again ");
408 LOGD(" Signal was " + signal);
409 longjmp(return_to_bottom, 1);
410}
411
412/// Возвращает директорию исполняемого файла.
413///
414/// Читает символическую ссылку /proc/self/exe и извлекает путь к
415/// родительской директории. Конфиг и лог-файл ищутся рядом с бинарником.
416/// @return Путь к директории с бинарником (без слеша на конце).
417static std::string getApplicationDirectory() {
418#ifdef _WIN32
419 return GetModuleFilename(nullptr);
420#else
421 char result[PATH_MAX];
422 ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
423 std::string appPath = std::string(result, (count > 0) ? count : 0);
424 std::size_t found = appPath.find_last_of("/\\");
425 return appPath.substr(0, found);
426#endif
427}
void daemonSleep()
Definition mailer.cpp:345
void CommitTransaction()
Фиксирует текущую транзакцию.
Definition mailer.cpp:334
Daemon()
Definition mailer.cpp:48
mINI::INIFile * iniFile
INI-файл конфигурации.
Definition mailer.cpp:181
void StartTransaction()
Создаёт новую транзакцию PostgreSQL.
Definition mailer.cpp:320
volatile std::atomic< bool > running
Флаг работы демона.
Definition mailer.cpp:190
std::string connectionStr
Строка подключения к PostgreSQL.
Definition mailer.cpp:184
void DisconnectPg()
Отключается от PostgreSQL.
Definition mailer.cpp:311
void AbortTransaction()
Откатывает текущую транзакцию.
Definition mailer.cpp:326
pqxx::connection * connection
Соединение с PostgreSQL.
Definition mailer.cpp:185
std::string version
Definition mailer.cpp:178
pqxx::work * trx
Текущая транзакция.
Definition mailer.cpp:186
std::string name
Definition mailer.cpp:173
Products::ProductsLog * productsLog
Логирование старта/стопа в БД.
Definition mailer.cpp:188
mINI::INIStructure ini
Структура INI-данных в памяти.
Definition mailer.cpp:182
void Stop()
Останавливает основной цикл демона.
Definition mailer.cpp:124
~Daemon()
Деструктор. Освобождает INI-файл, логгирует остановку, отключается от PG.
Definition mailer.cpp:104
void ConnectPg()
Definition mailer.cpp:286
void Run()
Definition mailer.cpp:134
int sleepPeriod
Пауза между итерациями (сек).
Definition mailer.cpp:191
int sendOrders(std::string tableName)
Definition mailer.cpp:201
void addCcEmailAddr(const std::string &email_addr, const std::string &name="")
Добавляет получателя в скрытую копию.
int send()
Отправляет письмо через SMTP (libcurl).
void addRecvEmailAddr(const std::string &email_addr, const std::string &name="")
Добавляет получателя в список "Кому".
void setEmailContent(const std::string &subject="", const std::string &body="")
Устанавливает тему и тело письма.
jmp_buf return_to_main
jmp_buf для graceful shutdown (SIGINT/SIGTERM).
Definition mailer.cpp:25
static std::string getApplicationDirectory()
Возвращает директорию, в которой находится исполняемый файл.
Definition mailer.cpp:417
static Daemon * d
Глобальный указатель на демон (доступен из signal handler'ов).
Definition mailer.cpp:355
jmp_buf return_to_bottom
jmp_buf для перезапуска цикла после SIGSEGV.
Definition mailer.cpp:27
#define FAILURE
Definition mailer.cpp:19
volatile std::sig_atomic_t gSignalStatus
Глобальный флаг последнего полученного сигнала.
Definition mailer.cpp:22
int main(void)
Definition mailer.cpp:363
#define SUCCESS
Definition mailer.cpp:18
static void sigIntHandler(int signal)
Обработчик сигналов завершения (SIGINT/SIGTERM/SIGABRT/SIGFPE/SIGILL).
Definition mailer.cpp:395
static void sigsegvhandler(int signal)
Обработчик segmentation fault — перезапускает цикл.
Definition mailer.cpp:406