From 2bd9eb64689aa53cb4c8012b7ee61240f4a85ce5 Mon Sep 17 00:00:00 2001 From: Relintai Date: Sat, 23 Dec 2023 00:50:09 +0100 Subject: [PATCH] Implemented SMTPClient based on https://github.com/Relintai/smtp_node . --- modules/web/smtp/smtp_client.cpp | 588 ++++++++++++++++++++++++++++++- modules/web/smtp/smtp_client.h | 108 ++++++ 2 files changed, 691 insertions(+), 5 deletions(-) diff --git a/modules/web/smtp/smtp_client.cpp b/modules/web/smtp/smtp_client.cpp index df9b3bd3b..79af0dee3 100644 --- a/modules/web/smtp/smtp_client.cpp +++ b/modules/web/smtp/smtp_client.cpp @@ -31,17 +31,595 @@ #include "smtp_client.h" -void SMTPClient::_notification(const int what) { +#include "core/bind/core_bind.h" +#include "core/io/ip.h" +#include "core/os/os.h" + +#define SMTP_DEBUG_ENABLED 0 + +SMTPClient::AuthType SMTPClient::get_auth_type() const { + return _auth_type; +} +void SMTPClient::set_auth_type(const AuthType value) { + _auth_type = value; +} + +String SMTPClient::get_smtp_server_address() const { + return _smtp_server_address; +} +void SMTPClient::set_smtp_server_address(const String &value) { + _smtp_server_address = value; +} + +int SMTPClient::get_smtp_server_port() const { + return _smtp_server_port; +} +void SMTPClient::set_smtp_server_port(const int value) { + _smtp_server_port = value; +} + +String SMTPClient::get_smtp_server_user_name() const { + return _smtp_server_user_name; +} +void SMTPClient::set_smtp_server_user_name(const String &value) { + _smtp_server_user_name = value; + + _smtp_server_user_name_base64 = _Marshalls::get_singleton()->utf8_to_base64(_smtp_server_user_name); +} + +String SMTPClient::get_smtp_server_password() const { + return _smtp_server_password; +} +void SMTPClient::set_smtp_server_password(const String &value) { + _smtp_server_password = value; + + _smtp_server_password_base64 = _Marshalls::get_singleton()->utf8_to_base64(_smtp_server_password); +} + +String SMTPClient::get_smtp_server_sender_email_address() const { + return _smtp_server_sender_email_address; +} +void SMTPClient::set_smtp_server_sender_email_address(const String &value) { + _smtp_server_sender_email_address = value; +} + +String SMTPClient::get_smtp_server_sender_name() const { + return _smtp_server_sender_name; +} +void SMTPClient::set_smtp_server_sender_name(const String &value) { + _smtp_server_sender_name = value; +} + +String SMTPClient::get_smtp_client_id() const { + return _smtp_client_id; +} +void SMTPClient::set_smtp_client_id(const String &value) { + _smtp_client_id = value; +} + +int SMTPClient::get_retries() const { + return _retries; +} +void SMTPClient::set_retries(const int value) { + _retries = value; +} + +int SMTPClient::get_delay() const { + return _delay; +} +void SMTPClient::set_delay(const int value) { + _delay = value; +} + +bool SMTPClient::get_use_threads() const { +#ifdef NO_THREADS + return false; +#else + return _use_threads; +#endif +} +void SMTPClient::set_use_threads(const bool value) { + _use_threads = value; + + _apply_thread_setting(); +} + +void SMTPClient::send_mail(const String &address, const String &subject, const String &message) { + MailData md; + md.address = address; + md.subject = subject; + md.message = message; + + if (get_use_threads()) { + _mail_queue_lock.lock(); + _mail_queue.push_back(md); + _mail_queue_lock.unlock(); + + _thread_semaphore.post(); + } else { + _send_mail(md); + } } SMTPClient::SMTPClient() { + _auth_type = AUTH_TYPE_SMTPS; + + _smtp_server_port = 465; + + _retries = 3; + _delay = 200; + _use_threads = true; + + _worker_thread = NULL; + _thread_running = false; } SMTPClient::~SMTPClient() { } -void SMTPClient::_bind_methods() { - ////ClassDB::bind_method(D_METHOD("get_uri_segment"), &SMTPClient::get_uri_segment); - //ClassDB::bind_method(D_METHOD("set_uri_segment", "val"), &SMTPClient::set_uri_segment); - //ADD_PROPERTY(PropertyInfo(Variant::STRING, "uri_segment"), "set_uri_segment", "get_uri_segment"); +void SMTPClient::_thread_main_loop(void *user_data) { + SMTPClient *self = (SMTPClient *)user_data; + + while (self->_thread_running) { + self->_mail_queue_lock.lock(); + + if (self->_mail_queue.size() > 0) { + int index = self->_mail_queue.size() - 1; + MailData d = self->_mail_queue[index]; + self->_mail_queue.remove(index); + + self->_mail_queue_lock.unlock(); + + self->_send_mail(d); + } else { + self->_mail_queue_lock.lock(); + } + + if (self->_mail_queue.size() == 0) { + self->_thread_semaphore.wait(); + } + } +} + +Error SMTPClient::_send_mail(const MailData &data) { + Error err = OK; + + err = _connect(); + + if (err == OK) { + err = _wait_for_answer(""); + } + + if (err == OK) { + err = _smtp_hello(); + } + + if (err == OK && _auth_type == AUTH_TYPE_STARTTLS) { + err = _smtp_starttls(); + + if (err == OK) { + err = _smtp_hello(); + } + } + + if (err == OK) { + err = _smtp_auth(); + } + + if (err == OK) { + err = _smtp_mail_from(); + } + + if (err == OK) { + err = _smtp_mail_to(data.address); + } + + if (err == OK) { + err = _smtp_mail_data(data.subject, data.message); + } + + if (err == OK) { + err = _smtp_quit(); + } + + _disconnect(); + +#if SMTP_DEBUG_ENABLED + if (err == OK) { + ERR_PRINT("Mail Sent"); + } else { + ERR_PRINT("ERROR sending mail: " + itos(err)); + } +#endif + + return err; +} + +Error SMTPClient::_connect() { + Error error = OK; + + if (_socket_tcp.is_valid()) { + return ERR_ALREADY_IN_USE; + } + + _socket_tcp.instance(); + error = _socket_tcp->connect_to_host(_smtp_server_address, _smtp_server_port); + +#if SMTP_DEBUG_ENABLED + ERR_PRINT("Connecting server..."); +#endif + + if (error != OK) { +#if SMTP_DEBUG_ENABLED + ERR_PRINT("Connection failed."); +#endif + + IP_Address ip = IP::get_singleton()->resolve_hostname(_smtp_server_address); + error = _socket_tcp->connect_to_host(ip, _smtp_server_port); +#if SMTP_DEBUG_ENABLED + ERR_PRINT("Trying with IP"); +#endif + } + + for (int i = 0; i < _retries; ++i) { +#if SMTP_DEBUG_ENABLED + ERR_PRINT("Try " + Streing::num(i) + " / " + String::num(_retries)); +#endif + + StreamPeerTCP::Status status = _socket_tcp->get_status(); + +#if SMTP_DEBUG_ENABLED + ERR_PRINT("StreamPeerSSL::STATUS: " + itos(status)); +#endif + if (status == StreamPeerTCP::STATUS_CONNECTED) { + break; + } + + _wait(); + } + + if (_socket_tcp->get_status() == StreamPeerTCP::STATUS_ERROR) { + return ERR_CANT_CONNECT; + } + + if (_auth_type == AUTH_TYPE_SMTPS) { + _socket_ssl = Ref(StreamPeerSSL::create()); + _socket_ssl->connect_to_stream(_socket_tcp, true, _smtp_server_address); + + for (int i = 0; i < _retries; ++i) { +#if SMTP_DEBUG_ENABLED + ERR_PRINT("SSL Try " + Streing::num(i) + " / " + String::num(_retries)); +#endif + + StreamPeerSSL::Status status = _socket_ssl->get_status(); + +#if SMTP_DEBUG_ENABLED + ERR_PRINT("StreamPeerSSL::STATUS: " + itos(status)); +#endif + + if (status == StreamPeerSSL::STATUS_CONNECTED) { + break; + } + + _wait(); + } + + if (_socket_ssl->get_status() == StreamPeerSSL::STATUS_ERROR) { + return ERR_CANT_CONNECT; + } + } + + return OK; +} +void SMTPClient::_disconnect() { + if (_socket_ssl.is_valid()) { + _socket_ssl->disconnect_from_stream(); + } + + _socket_tcp->disconnect_from_host(); + + _socket_ssl.unref(); + _socket_tcp.unref(); +} + +Error SMTPClient::_smtp_hello() { + Error err = _send("HELO " + _smtp_client_id); + _wait_for_answer(""); + err = _send("EHLO " + _smtp_client_id); + err = _wait_for_answer("250"); + + return err; +} +Error SMTPClient::_smtp_starttls() { + Error err = _send("STARTTLS"); + err = _wait_for_answer("220"); // 220 TLS go ahead + + if (err != OK) { + return err; + } + + _socket_ssl = Ref(StreamPeerSSL::create()); + + _socket_ssl->connect_to_stream(_socket_tcp, true, _smtp_server_address); + + for (int i = 0; i < _retries; ++i) { +#if SMTP_DEBUG_ENABLED + ERR_PRINT("SSL Try " + String::num(i) + " / " + String::num(_retries)); +#endif + + StreamPeerSSL::Status status = _socket_ssl->get_status(); + + if (status == StreamPeerSSL::STATUS_ERROR) { + return ERR_CANT_CONNECT; + } + +#if SMTP_DEBUG_ENABLED + ERR_PRINT("StreamPeerSSL::STATUS: " + itos(status)); +#endif + + if (status == StreamPeerSSL::STATUS_CONNECTED) { + break; + } + + _wait(); + } + + if (_socket_ssl->get_status() == StreamPeerSSL::STATUS_ERROR) { + return ERR_CANT_CONNECT; + } + + return OK; +} +Error SMTPClient::_smtp_auth() { + Error err = _send("AUTH LOGIN"); + err = _wait_for_answer("334"); + +#if SMTP_DEBUG_ENABLED + ERR_PRINT("_smtp_auth 1"); +#endif + + if (err == OK) { + err = _send(_smtp_server_user_name_base64); + } + + err = _wait_for_answer("334"); + +#if SMTP_DEBUG_ENABLED + ERR_PRINT("_smtp_auth 2"); +#endif + + if (err == OK) { + err = _send(_smtp_server_password_base64); + } + + err = _wait_for_answer("235"); + +#if SMTP_DEBUG_ENABLED + ERR_PRINT("_smtp_auth 3"); +#endif + + return err; +} +Error SMTPClient::_smtp_mail_from() { + Error err = _send("MAIL FROM: <" + _smtp_server_sender_email_address + ">"); + err = _wait_for_answer("250"); + return err; +} +Error SMTPClient::_smtp_mail_to(const String &recipient) { + Error err = _send("RCPT TO: <" + recipient + ">"); + err = _wait_for_answer("250"); + return err; +} +Error SMTPClient::_smtp_mail_data(const String &subject, const String &message) { + Error err = _send("DATA"); + + err = _wait_for_answer("354"); + + if (err == OK) { + err = _send("FROM: " + _smtp_server_sender_name + " <" + _smtp_server_sender_email_address + ">"); + //err = _wait_for_answer("250"); + } + + if (err == OK && !subject.empty()) { + err = _send("SUBJECT: " + subject); + //err = _wait_for_answer("250"); + } + + if (err == OK && !message.empty()) { + err = _send(message + "\r\n.\r\n"); + //err = _wait_for_answer("250"); + } + + err = _wait_for_answer("250"); + + return err; +} +Error SMTPClient::_smtp_quit() { + return _send("QUIT"); +} + +Error SMTPClient::_send(const String &data) { +#if SMTP_DEBUG_ENABLED + ERR_PRINT("_send: " + data); +#endif + + String s = data + "\n"; + Vector v = s.to_utf8_buffer(); + + if (_socket_ssl.is_valid()) { + return _socket_ssl->put_data(v.ptr(), v.size()); + } + + if (_socket_tcp.is_valid()) { + return _socket_tcp->put_data(v.ptr(), v.size()); + } + + return ERR_CANT_CONNECT; +} + +Error SMTPClient::_wait_for_answer(const String &expected_answer) { + String _read_buffer; + + _wait(); + + for (int i = 0; i < _retries; ++i) { + if (_socket_ssl.is_valid()) { + _socket_ssl->poll(); + } + + int available_bytes = 0; + + if (_socket_ssl.is_valid()) { + available_bytes = _socket_ssl->get_available_bytes(); + } else if (_socket_tcp.is_valid()) { + available_bytes = _socket_tcp->get_available_bytes(); + } else { + return ERR_CANT_CONNECT; + } + + if (available_bytes > 0) { + if (_socket_ssl.is_valid()) { + _read_buffer += _socket_ssl->get_utf8_string(); + } else if (_socket_tcp.is_valid()) { + _read_buffer += _socket_tcp->get_available_bytes(); + } else { + return ERR_CANT_CONNECT; + } + +#if SMTP_DEBUG_ENABLED + ERR_PRINT("_wait_for_answer received: " + _read_buffer); +#endif + + break; + } else { + _wait(); + } + } + + if (_read_buffer.empty()) { + return ERR_CANT_ACQUIRE_RESOURCE; + } + + if (_parse_answer_packet(_read_buffer, expected_answer) != OK) { + return FAILED; + } + + return OK; +} +Error SMTPClient::_parse_answer_packet(const String &data, const String &expected_answer) { + if (expected_answer.empty()) { + return OK; + } + + int slicecount = data.get_slice_count("\r\n"); + + if (slicecount <= 1) { + if (data.left(expected_answer.length()) == expected_answer) { + return OK; + } else { + return FAILED; + } + } else { + String ll = data.get_slice("\r\n", slicecount - 2); + if (ll.left(expected_answer.length()) == expected_answer) { + return OK; + } else { + return FAILED; + } + } +} + +void SMTPClient::_wait() { + OS::get_singleton()->delay_usec(int64_t(_delay) * 1000); +} + +void SMTPClient::_apply_thread_setting() { + if (!is_inside_tree()) { + return; + } + + if (get_use_threads() || !_worker_thread) { + _thread_running = true; + _worker_thread = memnew(Thread); + _worker_thread->start(_thread_main_loop, this); + } + + if (!get_use_threads() && _worker_thread) { + _thread_running = false; + _thread_semaphore.post(); + _worker_thread->wait_to_finish(); + memdelete(_worker_thread); + _worker_thread = NULL; + } +} + +void SMTPClient::_notification(const int what) { + switch (what) { + case NOTIFICATION_ENTER_TREE: { + if (get_use_threads()) { + _thread_running = true; + _worker_thread = memnew(Thread); + _worker_thread->start(_thread_main_loop, this); + } + } break; + case NOTIFICATION_EXIT_TREE: { + if (_worker_thread) { + _thread_running = false; + _thread_semaphore.post(); + _worker_thread->wait_to_finish(); + memdelete(_worker_thread); + _worker_thread = NULL; + } + } break; + } +} + +void SMTPClient::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_auth_type"), &SMTPClient::get_auth_type); + ClassDB::bind_method(D_METHOD("set_auth_type", "val"), &SMTPClient::set_auth_type); + ADD_PROPERTY(PropertyInfo(Variant::INT, "auth_type", PROPERTY_HINT_ENUM, "Plaintext,STARTTLS,SMTPS"), "set_auth_type", "get_auth_type"); + + ClassDB::bind_method(D_METHOD("get_smtp_server_address"), &SMTPClient::get_smtp_server_address); + ClassDB::bind_method(D_METHOD("set_smtp_server_address", "val"), &SMTPClient::set_smtp_server_address); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "smtp_server_address"), "set_smtp_server_address", "get_smtp_server_address"); + + ClassDB::bind_method(D_METHOD("get_smtp_server_port"), &SMTPClient::get_smtp_server_port); + ClassDB::bind_method(D_METHOD("set_smtp_server_port", "val"), &SMTPClient::set_smtp_server_port); + ADD_PROPERTY(PropertyInfo(Variant::INT, "smtp_server_port"), "set_smtp_server_port", "get_smtp_server_port"); + + ClassDB::bind_method(D_METHOD("get_smtp_server_user_name"), &SMTPClient::get_smtp_server_user_name); + ClassDB::bind_method(D_METHOD("set_smtp_server_user_name", "val"), &SMTPClient::set_smtp_server_user_name); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "smtp_server_user_name"), "set_smtp_server_user_name", "get_smtp_server_user_name"); + + ClassDB::bind_method(D_METHOD("get_smtp_server_password"), &SMTPClient::get_smtp_server_password); + ClassDB::bind_method(D_METHOD("set_smtp_server_password", "val"), &SMTPClient::set_smtp_server_password); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "smtp_server_password"), "set_smtp_server_password", "get_smtp_server_password"); + + ClassDB::bind_method(D_METHOD("get_smtp_server_sender_email_address"), &SMTPClient::get_smtp_server_sender_email_address); + ClassDB::bind_method(D_METHOD("set_smtp_server_sender_email_address", "val"), &SMTPClient::set_smtp_server_sender_email_address); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "smtp_server_sender_email_address"), "set_smtp_server_sender_email_address", "get_smtp_server_sender_email_address"); + + ClassDB::bind_method(D_METHOD("get_smtp_server_sender_name"), &SMTPClient::get_smtp_server_sender_name); + ClassDB::bind_method(D_METHOD("set_smtp_server_sender_name", "val"), &SMTPClient::set_smtp_server_sender_name); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "smtp_server_sender_name"), "set_smtp_server_sender_name", "get_smtp_server_sender_name"); + + ClassDB::bind_method(D_METHOD("get_smtp_client_id"), &SMTPClient::get_smtp_client_id); + ClassDB::bind_method(D_METHOD("set_smtp_client_id", "val"), &SMTPClient::set_smtp_client_id); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "smtp_client_id"), "set_smtp_client_id", "get_smtp_client_id"); + + ClassDB::bind_method(D_METHOD("get_retries"), &SMTPClient::get_retries); + ClassDB::bind_method(D_METHOD("set_retries", "val"), &SMTPClient::set_retries); + ADD_PROPERTY(PropertyInfo(Variant::INT, "retries"), "set_retries", "get_retries"); + + ClassDB::bind_method(D_METHOD("get_delay"), &SMTPClient::get_delay); + ClassDB::bind_method(D_METHOD("set_delay", "val"), &SMTPClient::set_delay); + ADD_PROPERTY(PropertyInfo(Variant::INT, "get_delay"), "set_delay", "get_delay"); + + ClassDB::bind_method(D_METHOD("get_use_threads"), &SMTPClient::get_use_threads); + ClassDB::bind_method(D_METHOD("set_use_threads", "val"), &SMTPClient::set_use_threads); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_threads"), "set_use_threads", "get_use_threads"); + + BIND_ENUM_CONSTANT(AUTH_TYPE_PLAINTEXT); + BIND_ENUM_CONSTANT(AUTH_TYPE_STARTTLS); + BIND_ENUM_CONSTANT(AUTH_TYPE_SMTPS); } diff --git a/modules/web/smtp/smtp_client.h b/modules/web/smtp/smtp_client.h index 9fbb3d929..905921758 100644 --- a/modules/web/smtp/smtp_client.h +++ b/modules/web/smtp/smtp_client.h @@ -32,19 +32,127 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#include "core/containers/vector.h" +#include "core/object/reference.h" +#include "core/io/stream_peer_ssl.h" +#include "core/io/stream_peer_tcp.h" +#include "core/os/mutex.h" +#include "core/os/semaphore.h" +#include "core/os/thread.h" + #include "scene/main/node.h" class SMTPClient : public Node { GDCLASS(SMTPClient, Node); public: + enum AuthType { + AUTH_TYPE_PLAINTEXT, // No SSL + AUTH_TYPE_STARTTLS, // Connect, then upgrade to SSL using the STARTTLS command before sending login information + AUTH_TYPE_SMTPS, // Just use SSL + }; + + AuthType get_auth_type() const; + void set_auth_type(const AuthType value); + + String get_smtp_server_address() const; + void set_smtp_server_address(const String &value); + + int get_smtp_server_port() const; + void set_smtp_server_port(const int value); + + String get_smtp_server_user_name() const; + void set_smtp_server_user_name(const String &value); + + String get_smtp_server_password() const; + void set_smtp_server_password(const String &value); + + String get_smtp_server_sender_email_address() const; + void set_smtp_server_sender_email_address(const String &value); + + String get_smtp_server_sender_name() const; + void set_smtp_server_sender_name(const String &value); + + String get_smtp_client_id() const; + void set_smtp_client_id(const String &value); + + int get_retries() const; + void set_retries(const int value); + + int get_delay() const; + void set_delay(const int value); + + bool get_use_threads() const; + void set_use_threads(const bool value); + + void send_mail(const String &address, const String &subject, const String &message); + SMTPClient(); ~SMTPClient(); protected: + struct MailData { + String address; + String subject; + String message; + }; + + static void _thread_main_loop(void *user_data); + Error _send_mail(const MailData &data); + + Error _connect(); + void _disconnect(); + + Error _smtp_hello(); + Error _smtp_starttls(); + Error _smtp_auth(); + Error _smtp_mail_from(); + Error _smtp_mail_to(const String &recipient); + Error _smtp_mail_data(const String &subject, const String &message); + Error _smtp_quit(); + + Error _send(const String &data); + + Error _wait_for_answer(const String &expected_answer); + Error _parse_answer_packet(const String &data, const String &expected_answer); + + void _wait(); + void _apply_thread_setting(); + void _notification(const int what); static void _bind_methods(); + + AuthType _auth_type; + + String _smtp_server_address; + int _smtp_server_port; + String _smtp_server_user_name; + String _smtp_server_password; + String _smtp_server_sender_email_address; + String _smtp_server_sender_name; + + // Can be anything, will be sent to the server at login. + String _smtp_client_id; + + int _retries; + int _delay; + bool _use_threads; + + String _smtp_server_user_name_base64; + String _smtp_server_password_base64; + + Ref _socket_tcp; + Ref _socket_ssl; + + Thread *_worker_thread; + Semaphore _thread_semaphore; + bool _thread_running; + + Vector _mail_queue; + Mutex _mail_queue_lock; }; +VARIANT_ENUM_CAST(SMTPClient::AuthType); + #endif