/*************************************************************************/ /* smtp_client.cpp */ /*************************************************************************/ /* This file is part of: */ /* PANDEMONIUM ENGINE'S SMTP MODULE */ /* https://github.com/Relintai/pandemonium_engine */ /*************************************************************************/ /* Copyright (c) 2022-present Péter Magyar. */ /* Copyright (c) 2021-2024 Nicolò Santilio */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ #include "smtp_client.h" #include "core/bind/core_bind.h" #include "core/config/engine.h" #include "core/io/stream_peer_ssl.h" #include "core/io/stream_peer_tcp.h" #include "core/log/logger.h" #include "core/os/os.h" #include "email.h" String SMTPClient::get_client_id() const { return client_id; } void SMTPClient::set_client_id(const String &p_value) { client_id = p_value; } String SMTPClient::get_host() const { return host; } void SMTPClient::set_host(const String &p_value) { host = p_value; } int SMTPClient::get_port() const { return port; } void SMTPClient::set_port(const int p_value) { port = p_value; } SMTPClient::TLSMethod SMTPClient::get_tls_method() const { return tls_method; } void SMTPClient::set_tls_method(const TLSMethod p_value) { tls_method = p_value; } String SMTPClient::get_server_auth_username() const { return server_auth_username; } void SMTPClient::set_server_auth_username(const String &p_value) { server_auth_username = p_value; } String SMTPClient::get_server_auth_password() const { return server_auth_password; } void SMTPClient::set_server_auth_password(const String &p_value) { server_auth_password = p_value; } SMTPClient::ServerAuthMethod SMTPClient::get_server_auth_method() const { return server_auth_method; } void SMTPClient::set_server_auth_method(const ServerAuthMethod p_value) { server_auth_method = p_value; } String SMTPClient::get_email_default_sender_email() const { return email_default_sender_email; } void SMTPClient::set_email_default_sender_email(const String &p_value) { email_default_sender_email = p_value; } String SMTPClient::get_email_default_sender_name() const { return email_default_sender_name; } void SMTPClient::set_email_default_sender_name(const String &p_value) { email_default_sender_name = p_value; } bool SMTPClient::get_use_threads() const { return _use_threads; } void SMTPClient::set_use_threads(const bool p_value) { if (_use_threads == p_value) { return; } if (Engine::get_singleton()->is_editor_hint()) { _use_threads = p_value; return; } if (is_inside_tree()) { if (should_use_threading()) { //currently using threads, will no longer be the case if (_worker_thread) { _worker_thread_running = false; //slow quit = let the current mail finish processing _worker_thread_fast_quit = false; _worker_semaphore.post(); _worker_thread->wait_to_finish(); memdelete(_worker_thread); _worker_thread = NULL; } } else { //currently not using threads, we want to if (should_use_threading()) { _worker_thread_running = true; _worker_thread = memnew(Thread); _worker_thread->start(_worker_thread_func, this); } } } _use_threads = p_value; } int SMTPClient::get_thread_sleep_usec() const { return thread_sleep_usec; } void SMTPClient::set_thread_sleep_usec(const int p_value) { thread_sleep_usec = p_value; } void SMTPClient::send_email(const Ref &p_email) { ERR_FAIL_COND(Engine::get_singleton()->is_editor_hint()); ERR_FAIL_COND(!is_inside_tree()); ERR_FAIL_COND(!p_email.is_valid()); if (should_use_threading()) { _mail_queue_mutex.lock(); _mail_queue.push_back(p_email); _mail_queue_mutex.unlock(); _worker_semaphore.post(); } else { if (_current_session_email.is_valid()) { _mail_queue.push_back(p_email); return; } _send_email(p_email); } } bool SMTPClient::should_use_threading() const { #if !defined(NO_THREADS) return _use_threads; #else return false; #endif } void SMTPClient::_send_email(const Ref &p_email) { _current_session_email = p_email; if (!_current_session_email.is_valid()) { return; } IP_Address ip; if (host.is_valid_ip_address()) { ip = host; } else { ip = IP::get_singleton()->resolve_hostname(host); } Error err = _tcp_client->connect_to_host(ip, port); if (err != OK) { PLOG_ERR("Could not connect! " + itos(err)); Dictionary error_body; error_body["message"] = "Error connecting to host."; error_body["code"] = err; emit_signal("error", error_body); Dictionary result; result["success"] = false; result["error"] = error_body; emit_signal("result", result); if (!should_use_threading()) { _no_thread_next_email(); } } _current_session_status = SESSION_STATUS_HELO; if (!should_use_threading()) { set_process(true); } } Error SMTPClient::poll_client() { if (_current_tls_started or _current_tls_established) { _tls_client->poll(); return OK; } else { return OK; } } bool SMTPClient::client_get_status() { if (_current_tls_started) { return _tls_client->get_status() == StreamPeerSSL::STATUS_CONNECTED; } return _tcp_client->get_status() == StreamPeerTCP::STATUS_CONNECTED; } int SMTPClient::client_get_available_bytes() { if (_current_tls_started) { return _tls_client->get_available_bytes(); } return _tcp_client->get_available_bytes(); } String SMTPClient::client_get_string(int bytes) { if (_current_tls_started) { return _tls_client->get_string(bytes); } return _tcp_client->get_string(bytes); } bool SMTPClient::start_auth() { if (server_auth_method == SERVER_AUTH_PLAIN) { _current_session_status = SESSION_STATUS_AUTHENTICATED; return true; } if (!write_command("AUTH LOGIN")) { return false; } _current_session_status = SESSION_STATUS_AUTH_LOGIN; return true; } bool SMTPClient::start_hello() { //_current_session_status = SESSION_STATUS_HELO if (!write_command("HELO " + client_id)) { return false; } _current_session_status = SESSION_STATUS_HELO_ACK; return true; } Error SMTPClient::client_put_data(const String &data) { Vector buffer = data.to_utf8_buffer(); if (_current_tls_established) { return _tls_client->put_data(buffer.ptr(), buffer.size()); } return _tcp_client->put_data(buffer.ptr(), buffer.size()); } bool SMTPClient::write_command(const String &command) { //PLOG_ERR("COMMAND: " + command + "\n"); Error err = client_put_data(command + "\n"); if (err != OK) { _current_session_status = SESSION_STATUS_COMMAND_NOT_SENT; Dictionary error_body; error_body["message"] = "Session error on command: " + command; error_body["code"] = err; emit_signal("error", error_body); Dictionary result; result["success"] = false; result["error"] = error_body; emit_signal("result", result); } return (err == OK); } Error SMTPClient::write_data(const String &data) { return client_put_data(data + "\r\n.\r\n"); } void SMTPClient::close_connection() { _current_session_status = SESSION_STATUS_NONE; _tls_client->disconnect_from_stream(); _tcp_client->disconnect_from_host(); _current_session_email.unref(); _current_to_index = 0; _current_tls_started = false; _current_tls_established = false; if (!should_use_threading()) { _no_thread_next_email(); } } String SMTPClient::encode_username() { return _Marshalls::get_singleton()->utf8_to_base64(server_auth_username); } String SMTPClient::encode_password() { return _Marshalls::get_singleton()->utf8_to_base64(server_auth_password); } void SMTPClient::_process_email() { if (_current_session_status == SESSION_STATUS_SERVER_ERROR) { close_connection(); } if (poll_client() == OK) { bool connected = client_get_status(); if (connected) { // We need to wait until the connection is properly set up before starting ssl if (tls_method == TLS_METHOD_SMTPS && !_current_tls_started) { Error err = _tls_client->connect_to_stream(_tcp_client, false, host); if (err != OK) { _current_session_status = SESSION_STATUS_SERVER_ERROR; Dictionary error_body; error_body["message"] = "Error connecting to TLS Stream."; error_body["code"] = err; emit_signal("error", error_body); Dictionary result; result["success"] = false; result["error"] = error_body; emit_signal("result", result); if (!should_use_threading()) { _no_thread_next_email(); } return; } _current_tls_started = true; _current_tls_established = true; } int bytes = client_get_available_bytes(); if (bytes > 0) { String msg = client_get_string(bytes); //PLOG_ERR("RECEIVED: " + msg) String code = msg.left(3); if (code == "220") { if (_current_session_status == SESSION_STATUS_HELO) { start_hello(); } else if (_current_session_status == SESSION_STATUS_STARTTLS) { Error err = _tls_client->connect_to_stream(_tcp_client, false, host); if (err != OK) { _current_session_status = SESSION_STATUS_SERVER_ERROR; Dictionary error_body; error_body["message"] = "Error connecting to TLS Stream."; error_body["code"] = err; emit_signal("error", error_body); Dictionary result; result["success"] = false; result["error"] = error_body; emit_signal("result", result); return; } _current_tls_started = true; _current_tls_established = true; // We need to do HELO + EHLO again _current_session_status = SESSION_STATUS_HELO; start_hello(); } } else if (code == "250") { if (_current_session_status == SESSION_STATUS_HELO_ACK) { if (!write_command("EHLO " + client_id)) { return; } _current_session_status = SESSION_STATUS_EHLO_ACK; } else if (_current_session_status == SESSION_STATUS_EHLO_ACK) { if (tls_method == TLS_METHOD_STARTTLS) { if (_current_tls_started) { // second round of HELO + EHLO done if (!start_auth()) { return; } } else { if (!write_command("STARTTLS")) { return; } _current_session_status = SESSION_STATUS_STARTTLS; } } else { if (!start_auth()) { return; } } } else if (_current_session_status == SESSION_STATUS_MAIL_FROM) { if (_current_to_index < _current_session_email->get_recipient_count()) { if (!write_command("RCPT TO: <" + _current_session_email->get_recipient_address(_current_to_index) + ">")) { return; } _current_to_index += 1; } if (_current_cc_index < _current_session_email->get_cc_count()) { if (!write_command("RCPT TO: <" + _current_session_email->get_cc_address(_current_cc_index) + ">")) { return; } _current_cc_index += 1; } _current_session_status = SESSION_STATUS_RCPT_TO; } else if (_current_session_status == SESSION_STATUS_RCPT_TO) { if (_current_to_index < _current_session_email->get_recipient_count()) { _current_session_status = SESSION_STATUS_MAIL_FROM; return; } if (_current_cc_index < _current_session_email->get_cc_count()) { _current_session_status = SESSION_STATUS_MAIL_FROM; return; } if (!write_command("DATA")) { return; } _current_session_status = SESSION_STATUS_DATA; } else if (_current_session_status == SESSION_STATUS_DATA_ACK) { if (!write_command("QUIT")) { return; } _current_session_status = SESSION_STATUS_QUIT; } } else if (code == "221") { if (_current_session_status == SESSION_STATUS_QUIT) { close_connection(); emit_signal("email_sent"); Dictionary result; result["success"] = true; emit_signal("result", result); } } else if (code == "235") { // Authentication Succeeded if (_current_session_status == SESSION_STATUS_PASSWORD) { _current_session_status = SESSION_STATUS_AUTHENTICATED; } } else if (code == "334") { if (_current_session_status == SESSION_STATUS_AUTH_LOGIN) { if (msg.begins_with("334 VXNlcm5hbWU6")) { if (!write_command(encode_username())) { return; } _current_session_status = SESSION_STATUS_USERNAME; } } else if (_current_session_status == SESSION_STATUS_USERNAME) { if (msg.begins_with("334 UGFzc3dvcmQ6")) { if (!write_command(encode_password())) { return; } _current_session_status = SESSION_STATUS_PASSWORD; } } } else if (code == "354") { if (_current_session_status == SESSION_STATUS_DATA) { if (!(write_data(_current_session_email->get_email_data_string(email_default_sender_name, email_default_sender_email)) == OK)) { _current_session_status = SESSION_STATUS_SERVER_ERROR; return; } _current_session_status = SESSION_STATUS_DATA_ACK; } } else { PLOG_ERR(msg); } } } if (_current_session_email.is_valid() && (_current_session_status == SESSION_STATUS_AUTHENTICATED)) { _current_session_status = SESSION_STATUS_MAIL_FROM; String sender_address = _current_session_email->get_sender_address(); String fn; if (sender_address.size() > 0) { fn = "<" + sender_address + ">"; } else { fn = "<" + email_default_sender_email + ">"; } if (!write_command("MAIL FROM: " + fn)) { return; } } else { return; } } else { PLOG_ERR("Couldn't poll!") } } void SMTPClient::_no_thread_next_email() { if (should_use_threading()) { return; } Ref mail; int size = _mail_queue.size(); if (size > 0) { mail = _mail_queue[0]; _mail_queue.remove(0); } else { set_process(false); return; } if (mail.is_valid()) { _send_email(mail); } } void SMTPClient::_worker_thread_func(void *user_data) { SMTPClient *self = (SMTPClient *)user_data; while (self->_worker_thread_running) { Ref _mail; self->_mail_queue_mutex.lock(); int size = self->_mail_queue.size(); if (size > 0) { _mail = self->_mail_queue[0]; self->_mail_queue.remove(0); } self->_mail_queue_mutex.unlock(); if (_mail.is_valid()) { self->_send_email(_mail); } while (self->_current_session_email.is_valid()) { OS::get_singleton()->delay_usec(self->thread_sleep_usec); // Early return if we want to quit if (!self->_worker_thread_running && self->_worker_thread_fast_quit) { self->close_connection(); return; } self->_process_email(); } if (!self->_worker_thread_running) { return; } if (self->_mail_queue.size() == 0) { self->_worker_semaphore.wait(); } } } SMTPClient::SMTPClient() { client_id = "smtp.pandemoniumengine.org"; port = 465; tls_method = TLS_METHOD_SMTPS; server_auth_method = SERVER_AUTH_LOGIN; _use_threads = true; _worker_thread = NULL; // 10 msec thread_sleep_usec = 10000; // Networking _tcp_client.instance(); _tls_client = Ref(StreamPeerSSL::create()); //SessionStatus _current_session_status = SESSION_STATUS_NONE; _current_to_index = 0; _current_cc_index = 0; _current_tls_started = false; _current_tls_established = false; // Threading _worker_thread_running = false; _worker_thread_fast_quit = false; set_process(false); } SMTPClient::~SMTPClient() { } void SMTPClient::_notification(int p_what) { switch (p_what) { case NOTIFICATION_PROCESS: { if (Engine::get_singleton()->is_editor_hint()) { set_process(false); return; } if (should_use_threading()) { set_process(false); return; } _process_email(); } break; case NOTIFICATION_ENTER_TREE: { if (Engine::get_singleton()->is_editor_hint()) { set_process(false); return; } set_process(false); _worker_thread_fast_quit = false; if (should_use_threading()) { _worker_thread_running = true; _worker_thread = memnew(Thread); _worker_thread->start(_worker_thread_func, this); } } break; case NOTIFICATION_EXIT_TREE: { if (Engine::get_singleton()->is_editor_hint()) { set_process(false); return; } if (_worker_thread) { _worker_thread_running = false; _worker_thread_fast_quit = true; _worker_semaphore.post(); _worker_thread->wait_to_finish(); memdelete(_worker_thread); _worker_thread = NULL; } } break; } } void SMTPClient::_bind_methods() { ADD_SIGNAL(MethodInfo("error", PropertyInfo(Variant::DICTIONARY, "error"))); ADD_SIGNAL(MethodInfo("email_sent")); ADD_SIGNAL(MethodInfo("result", PropertyInfo(Variant::DICTIONARY, "content"))); ClassDB::bind_method(D_METHOD("get_client_id"), &SMTPClient::get_client_id); ClassDB::bind_method(D_METHOD("set_client_id", "val"), &SMTPClient::set_client_id); ADD_PROPERTY(PropertyInfo(Variant::STRING, "client_id"), "set_client_id", "get_client_id"); ClassDB::bind_method(D_METHOD("get_host"), &SMTPClient::get_host); ClassDB::bind_method(D_METHOD("set_host", "val"), &SMTPClient::set_host); ADD_PROPERTY(PropertyInfo(Variant::STRING, "host"), "set_host", "get_host"); ClassDB::bind_method(D_METHOD("get_port"), &SMTPClient::get_port); ClassDB::bind_method(D_METHOD("set_port", "val"), &SMTPClient::set_port); ADD_PROPERTY(PropertyInfo(Variant::INT, "port"), "set_port", "get_port"); ClassDB::bind_method(D_METHOD("get_tls_method"), &SMTPClient::get_tls_method); ClassDB::bind_method(D_METHOD("set_tls_method", "val"), &SMTPClient::set_tls_method); ADD_PROPERTY(PropertyInfo(Variant::INT, "tls_method", PROPERTY_HINT_ENUM, "NONE,STARTTLS,SMTPS"), "set_tls_method", "get_tls_method"); ClassDB::bind_method(D_METHOD("get_server_auth_username"), &SMTPClient::get_server_auth_username); ClassDB::bind_method(D_METHOD("set_server_auth_username", "val"), &SMTPClient::set_server_auth_username); ADD_PROPERTY(PropertyInfo(Variant::STRING, "server_auth_username"), "set_server_auth_username", "get_server_auth_username"); ClassDB::bind_method(D_METHOD("get_server_auth_password"), &SMTPClient::get_server_auth_password); ClassDB::bind_method(D_METHOD("set_server_auth_password", "val"), &SMTPClient::set_server_auth_password); ADD_PROPERTY(PropertyInfo(Variant::STRING, "server_auth_password"), "set_server_auth_password", "get_server_auth_password"); ClassDB::bind_method(D_METHOD("get_server_auth_method"), &SMTPClient::get_server_auth_method); ClassDB::bind_method(D_METHOD("set_server_auth_method", "val"), &SMTPClient::set_server_auth_method); ADD_PROPERTY(PropertyInfo(Variant::INT, "server_auth_method", PROPERTY_HINT_ENUM, "Plain,Login"), "set_server_auth_method", "get_server_auth_method"); ClassDB::bind_method(D_METHOD("get_email_default_sender_email"), &SMTPClient::get_email_default_sender_email); ClassDB::bind_method(D_METHOD("set_email_default_sender_email", "val"), &SMTPClient::set_email_default_sender_email); ADD_PROPERTY(PropertyInfo(Variant::STRING, "email_default_sender_email"), "set_email_default_sender_email", "get_email_default_sender_email"); ClassDB::bind_method(D_METHOD("get_email_default_sender_name"), &SMTPClient::get_email_default_sender_name); ClassDB::bind_method(D_METHOD("set_email_default_sender_name", "val"), &SMTPClient::set_email_default_sender_name); ADD_PROPERTY(PropertyInfo(Variant::STRING, "email_default_sender_name"), "set_email_default_sender_name", "get_email_default_sender_name"); 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"); ClassDB::bind_method(D_METHOD("get_thread_sleep_usec"), &SMTPClient::get_thread_sleep_usec); ClassDB::bind_method(D_METHOD("set_thread_sleep_usec", "val"), &SMTPClient::set_thread_sleep_usec); ADD_PROPERTY(PropertyInfo(Variant::INT, "thread_sleep_usec"), "set_thread_sleep_usec", "get_thread_sleep_usec"); ClassDB::bind_method(D_METHOD("send_email", "email"), &SMTPClient::send_email); BIND_ENUM_CONSTANT(TLS_METHOD_NONE); BIND_ENUM_CONSTANT(TLS_METHOD_STARTTLS); BIND_ENUM_CONSTANT(TLS_METHOD_SMTPS); BIND_ENUM_CONSTANT(SERVER_AUTH_PLAIN); BIND_ENUM_CONSTANT(SERVER_AUTH_LOGIN); }