local socket = require "socket"; local ssl = require "ssl"; local b64 = require "prosody.util.encodings".base64.encode; local smtp_server = module:get_option_string("offline_email_smtp_server"); local smtp_port = module:get_option_number("offline_email_smtp_port", 587); local smtp_username = module:get_option_string("offline_email_smtp_username"); local smtp_password = module:get_option_string("offline_email_smtp_password"); local smtp_from = module:get_option_string("offline_email_smtp_from", smtp_username); local accounts = module:open_store("accounts"); local function read_response(sock) local lines = {}; while true do local line, err = sock:receive("*l"); if not line then return nil, err; end table.insert(lines, line); if line:sub(4, 4) == " " then break; end end local code = tonumber(lines[1]:sub(1, 3)); return code, table.concat(lines, "\n"); end local function send_command(sock, cmd) sock:send(cmd .. "\r\n"); return read_response(sock); end local function send_email(to_email, subject, body_text) local sock = socket.tcp(); sock:settimeout(10); local ok, err = sock:connect(smtp_server, smtp_port); if not ok then return nil, "connect: " .. tostring(err); end local code, resp = read_response(sock); if not code or code ~= 220 then sock:close(); return nil, "greeting: " .. tostring(resp); end code = send_command(sock, "EHLO localhost"); if code ~= 250 then sock:close(); return nil, "EHLO failed"; end code = send_command(sock, "STARTTLS"); if code ~= 220 then sock:close(); return nil, "STARTTLS rejected"; end sock = ssl.wrap(sock, { mode = "client", protocol = "any", verify = "none" }); ok = sock:dohandshake(); if not ok then sock:close(); return nil, "TLS handshake failed"; end code = send_command(sock, "EHLO localhost"); if code ~= 250 then sock:close(); return nil, "EHLO after TLS failed"; end local auth_str = b64("\0" .. smtp_username .. "\0" .. smtp_password); code = send_command(sock, "AUTH PLAIN " .. auth_str); if code ~= 235 then sock:close(); return nil, "AUTH failed"; end code = send_command(sock, "MAIL FROM:<" .. smtp_from .. ">"); if code ~= 250 then sock:close(); return nil, "MAIL FROM failed"; end code = send_command(sock, "RCPT TO:<" .. to_email .. ">"); if code ~= 250 then sock:close(); return nil, "RCPT TO failed"; end code = send_command(sock, "DATA"); if code ~= 354 then sock:close(); return nil, "DATA failed"; end local message = "From: " .. smtp_from .. "\r\n" .. "To: " .. to_email .. "\r\n" .. "Subject: " .. subject .. "\r\n" .. "Content-Type: text/plain; charset=UTF-8\r\n" .. "\r\n" .. body_text .. "\r\n.\r\n"; sock:send(message); code = read_response(sock); if code ~= 250 then sock:close(); return nil, "message rejected"; end send_command(sock, "QUIT"); sock:close(); return true; end local function notify_offline(to_user, stanza) local body = stanza:get_child_text("body"); local is_encrypted = stanza:get_child("encrypted", "eu.siacs.conversations.axolotl") ~= nil; if not is_encrypted and (not body or body == "") then return; end local account = accounts:get(to_user); if not account or not account.email then return; end local from_jid = stanza.attr.from or "unknown"; local from_name = from_jid:match("^([^@]+)") or from_jid; local subject = "New message from " .. from_name; local email_body; if is_encrypted then email_body = "Ahoy,\r\n\r\n" .. "while offline, you've received an encrypted message from " .. from_name .. ".\r\n\r\n" .. "This message was sent using end-to-end encryption (OMEMO).\r\n" .. "To view it, log in from the same device where your encryption keys are stored. " .. "The message cannot be read on a different device or client.\r\n\r\n" .. "Kind regards,\r\n\r\n" .. "Tiara"; else email_body = "Ahoy,\r\n\r\n" .. "while offline, you received a plain-text message from " .. from_name .. ":\r\n\r\n" .. ">> " .. body .. "\r\n\r\n" .. "Kind regards,\r\n\r\n" .. "Tiara"; end local ok, err = send_email(account.email, subject, email_body); if ok then module:log("info", "Sent offline email notification to %s for user %s", account.email, to_user); else module:log("warn", "Failed to send offline email to %s: %s", account.email, tostring(err)); end end local bare_sessions = prosody.bare_sessions; local jid = require "util.jid"; local function has_hibernating_session(username) local bare = username .. "@" .. module.host; local user = bare_sessions[bare]; if not user then return false; end for _, session in pairs(user.sessions) do if session.hibernating then return true; end end return false; end -- Fired when user is offline (no sessions) or when smacks gives up on a hibernating session module:hook("message/offline/handle", function(event) local to_user = event.username or (event.stanza.attr.to and event.stanza.attr.to:match("^([^@]+)")); if not to_user then return; end -- Skip if smacks is still hibernating — we'll get called again when it expires if has_hibernating_session(to_user) then module:log("debug", "Skipping email for %s — smacks session still hibernating", to_user); return; end notify_offline(to_user, event.stanza); end, 1);