Ported SMTPClient.

This commit is contained in:
Relintai 2024-01-11 00:02:48 +01:00
parent fb4fe82c63
commit 8541890a78
2 changed files with 837 additions and 4 deletions

View File

@ -30,14 +30,699 @@
#include "smtp_client.h" #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<EMail> &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 {
_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<EMail> &p_email) {
_current_session_email = p_email;
if (!_current_session_email.is_valid()) {
return;
}
Error err = _tcp_client->connect_to_host(host, 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()) {
set_process(false);
}
}
if (tls_method == TLS_METHOD_SMTPS) {
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()) {
set_process(false);
}
return;
}
_current_tls_started = true;
_current_tls_established = true;
}
_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 CharString &data) {
if (_current_tls_established) {
return _tls_client->put_data((const uint8_t *)data.ptr(), data.size());
}
return _tcp_client->put_data((const uint8_t *)data.ptr(), data.size());
}
bool SMTPClient::write_command(const String &command) {
//print("COMMAND: " + command)
Error err = client_put_data((command + "\n").utf8());
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").utf8());
}
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()) {
set_process(false);
}
}
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) {
int bytes = client_get_available_bytes();
if (bytes > 0) {
String msg = client_get_string(bytes);
//print("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::_worker_thread_func(void *user_data) {
SMTPClient *self = (SMTPClient *)user_data;
while (self->_worker_thread_running) {
Ref<EMail> _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() { 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>(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() { SMTPClient::~SMTPClient() {
} }
void SMTPClient::_bind_methods() { void SMTPClient::_notification(int p_what) {
//ClassDB::bind_method(D_METHOD("get_ignored_urls"), &SMTPClient::get_ignored_urls); switch (p_what) {
//ClassDB::bind_method(D_METHOD("set_ignored_urls", "val"), &SMTPClient::set_ignored_urls); case NOTIFICATION_PROCESS: {
//ADD_PROPERTY(PropertyInfo(Variant::POOL_STRING_ARRAY, "ignored_urls"), "set_ignored_urls", "get_ignored_urls"); 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);
} }

View File

@ -31,17 +31,165 @@
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/ /*************************************************************************/
#include "core/object/reference.h"
#include "core/os/mutex.h"
#include "core/os/semaphore.h"
#include "core/os/thread.h"
#include "scene/main/node.h" #include "scene/main/node.h"
class EMail;
class StreamPeerTCP;
class StreamPeerSSL;
class SMTPClient : public Node { class SMTPClient : public Node {
GDCLASS(SMTPClient, Node); GDCLASS(SMTPClient, Node);
public: public:
enum TLSMethod {
TLS_METHOD_NONE = 0,
TLS_METHOD_STARTTLS,
TLS_METHOD_SMTPS,
};
enum ServerAuthMethod {
SERVER_AUTH_PLAIN,
SERVER_AUTH_LOGIN
};
String get_client_id() const;
void set_client_id(const String &p_value);
String get_host() const;
void set_host(const String &p_value);
int get_port() const;
void set_port(const int p_value);
TLSMethod get_tls_method() const;
void set_tls_method(const TLSMethod p_value);
String get_server_auth_username() const;
void set_server_auth_username(const String &p_value);
String get_server_auth_password() const;
void set_server_auth_password(const String &p_value);
ServerAuthMethod get_server_auth_method() const;
void set_server_auth_method(const ServerAuthMethod p_value);
String get_email_default_sender_email() const;
void set_email_default_sender_email(const String &p_value);
String get_email_default_sender_name() const;
void set_email_default_sender_name(const String &p_value);
bool get_use_threads() const;
void set_use_threads(const bool p_value);
int get_thread_sleep_usec() const;
void set_thread_sleep_usec(const int p_value);
void send_email(const Ref<EMail> &p_email);
SMTPClient(); SMTPClient();
~SMTPClient(); ~SMTPClient();
protected: protected:
enum SessionStatus {
SESSION_STATUS_NONE = 0,
SESSION_STATUS_SERVER_ERROR,
SESSION_STATUS_COMMAND_NOT_SENT,
SESSION_STATUS_COMMAND_REFUSED,
SESSION_STATUS_HELO,
SESSION_STATUS_HELO_ACK,
SESSION_STATUS_EHLO,
SESSION_STATUS_EHLO_ACK,
SESSION_STATUS_MAIL_FROM,
SESSION_STATUS_RCPT_TO,
SESSION_STATUS_DATA,
SESSION_STATUS_DATA_ACK,
SESSION_STATUS_QUIT,
SESSION_STATUS_STARTTLS,
SESSION_STATUS_STARTTLS_ACK,
SESSION_STATUS_AUTH_LOGIN,
SESSION_STATUS_USERNAME,
SESSION_STATUS_PASSWORD,
SESSION_STATUS_AUTHENTICATED
};
bool should_use_threading() const;
void _send_email(const Ref<EMail> &p_email);
Error poll_client();
bool client_get_status();
int client_get_available_bytes();
String client_get_string(int bytes);
bool start_auth();
bool start_hello();
Error client_put_data(const CharString &data);
bool write_command(const String &command);
Error write_data(const String &data);
void close_connection();
String encode_username();
String encode_password();
void _process_email();
static void _worker_thread_func(void *user_data);
void _notification(int p_what);
static void _bind_methods(); static void _bind_methods();
String client_id;
String host;
int port;
TLSMethod tls_method;
String server_auth_username;
String server_auth_password;
ServerAuthMethod server_auth_method;
String email_default_sender_email;
String email_default_sender_name;
bool _use_threads;
int thread_sleep_usec;
// Networking
Ref<StreamPeerSSL> _tls_client;
Ref<StreamPeerTCP> _tcp_client;
// SessionStatus
SessionStatus _current_session_status;
Ref<EMail> _current_session_email;
int _current_to_index;
int _current_cc_index;
bool _current_tls_started;
bool _current_tls_established;
// Threading
bool _worker_thread_running;
bool _worker_thread_fast_quit;
Thread *_worker_thread;
Semaphore _worker_semaphore;
Mutex _mail_queue_mutex;
Vector<Ref<EMail>> _mail_queue;
}; };
VARIANT_ENUM_CAST(SMTPClient::TLSMethod);
VARIANT_ENUM_CAST(SMTPClient::ServerAuthMethod);
#endif #endif