Class: Tina4::Messenger
- Inherits:
-
Object
- Object
- Tina4::Messenger
- Defined in:
- lib/tina4/messenger.rb
Overview
Tina4 Messenger — Email sending (SMTP) and reading (IMAP).
Unified .env-driven configuration with constructor override. Priority: constructor params > .env (TINA4_MAIL_* with SMTP_* fallback) > sensible defaults
# .env
TINA4_MAIL_HOST=smtp.gmail.com
TINA4_MAIL_PORT=587
TINA4_MAIL_USERNAME=user@gmail.com
TINA4_MAIL_PASSWORD=app-password
TINA4_MAIL_FROM=noreply@myapp.com
TINA4_MAIL_ENCRYPTION=tls
TINA4_MAIL_IMAP_HOST=imap.gmail.com
TINA4_MAIL_IMAP_PORT=993
mail = Messenger.new # reads from .env
mail = Messenger.new(host: "smtp.office365.com", port: 587) # override
mail.send(to: "user@test.com", subject: "Welcome", body: "<h1>Hello!</h1>", html: true, text: "Hello!")
Instance Attribute Summary collapse
-
#encryption ⇒ Object
readonly
Returns the value of attribute encryption.
-
#from_address ⇒ Object
readonly
Returns the value of attribute from_address.
-
#from_name ⇒ Object
readonly
Returns the value of attribute from_name.
-
#host ⇒ Object
readonly
Returns the value of attribute host.
-
#imap_host ⇒ Object
readonly
Returns the value of attribute imap_host.
-
#imap_port ⇒ Object
readonly
Returns the value of attribute imap_port.
-
#port ⇒ Object
readonly
Returns the value of attribute port.
-
#use_tls ⇒ Object
readonly
Returns the value of attribute use_tls.
-
#username ⇒ Object
readonly
Returns the value of attribute username.
Instance Method Summary collapse
-
#folders ⇒ Object
List all IMAP folders.
-
#inbox(folder: "INBOX", limit: 20, offset: 0) ⇒ Object
List messages in a folder.
-
#initialize(host: nil, port: nil, username: nil, password: nil, from_address: nil, from_name: nil, encryption: nil, use_tls: nil, imap_host: nil, imap_port: nil) ⇒ Messenger
constructor
Initialize with SMTP config.
-
#mark_read(uid, folder: "INBOX") ⇒ Object
Mark a message as read (set Seen flag).
-
#read(uid, folder: "INBOX", mark_read: true) ⇒ Object
Read a single message by UID.
-
#search(folder: "INBOX", subject: nil, sender: nil, since: nil, before: nil, unseen_only: false, limit: 20) ⇒ Object
Search messages with filters.
-
#send(to:, subject:, body:, html: false, text: nil, cc: [], bcc: [], reply_to: nil, attachments: [], headers: {}) ⇒ Object
Send email using Ruby’s Net::SMTP Returns { success: true/false, message: “…”, id: “…” }.
-
#test_connection ⇒ Object
Test SMTP connection Returns { success: true/false, message: “…” }.
-
#test_imap_connection ⇒ Hash
Test IMAP connectivity without reading messages.
-
#unread(folder: "INBOX") ⇒ Object
Count unread messages.
Constructor Details
#initialize(host: nil, port: nil, username: nil, password: nil, from_address: nil, from_name: nil, encryption: nil, use_tls: nil, imap_host: nil, imap_port: nil) ⇒ Messenger
Initialize with SMTP config. Priority: constructor params > ENV (TINA4_MAIL_* with SMTP_* fallback) > sensible defaults
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/tina4/messenger.rb', line 43 def initialize(host: nil, port: nil, username: nil, password: nil, from_address: nil, from_name: nil, encryption: nil, use_tls: nil, imap_host: nil, imap_port: nil) @host = host || ENV["TINA4_MAIL_HOST"] || ENV["SMTP_HOST"] || "localhost" @port = (port || ENV["TINA4_MAIL_PORT"] || ENV["SMTP_PORT"] || 587).to_i @username = username || ENV["TINA4_MAIL_USERNAME"] || ENV["SMTP_USERNAME"] @password = password || ENV["TINA4_MAIL_PASSWORD"] || ENV["SMTP_PASSWORD"] resolved_from = from_address || ENV["TINA4_MAIL_FROM"] || ENV["SMTP_FROM"] @from_address = resolved_from || @username || "noreply@localhost" @from_name = from_name || ENV["TINA4_MAIL_FROM_NAME"] || ENV["SMTP_FROM_NAME"] || "" # Encryption: constructor > .env > backward-compat use_tls > default "tls" env_encryption = encryption || ENV["TINA4_MAIL_ENCRYPTION"] if env_encryption @encryption = env_encryption.downcase elsif !use_tls.nil? @encryption = use_tls ? "tls" : "none" else @encryption = "tls" end @use_tls = %w[tls starttls].include?(@encryption) @imap_host = imap_host || ENV["TINA4_MAIL_IMAP_HOST"] || ENV["IMAP_HOST"] || @host @imap_port = (imap_port || ENV["TINA4_MAIL_IMAP_PORT"] || ENV["IMAP_PORT"] || 993).to_i end |
Instance Attribute Details
#encryption ⇒ Object (readonly)
Returns the value of attribute encryption.
38 39 40 |
# File 'lib/tina4/messenger.rb', line 38 def encryption @encryption end |
#from_address ⇒ Object (readonly)
Returns the value of attribute from_address.
38 39 40 |
# File 'lib/tina4/messenger.rb', line 38 def from_address @from_address end |
#from_name ⇒ Object (readonly)
Returns the value of attribute from_name.
38 39 40 |
# File 'lib/tina4/messenger.rb', line 38 def from_name @from_name end |
#host ⇒ Object (readonly)
Returns the value of attribute host.
38 39 40 |
# File 'lib/tina4/messenger.rb', line 38 def host @host end |
#imap_host ⇒ Object (readonly)
Returns the value of attribute imap_host.
38 39 40 |
# File 'lib/tina4/messenger.rb', line 38 def imap_host @imap_host end |
#imap_port ⇒ Object (readonly)
Returns the value of attribute imap_port.
38 39 40 |
# File 'lib/tina4/messenger.rb', line 38 def imap_port @imap_port end |
#port ⇒ Object (readonly)
Returns the value of attribute port.
38 39 40 |
# File 'lib/tina4/messenger.rb', line 38 def port @port end |
#use_tls ⇒ Object (readonly)
Returns the value of attribute use_tls.
38 39 40 |
# File 'lib/tina4/messenger.rb', line 38 def use_tls @use_tls end |
#username ⇒ Object (readonly)
Returns the value of attribute username.
38 39 40 |
# File 'lib/tina4/messenger.rb', line 38 def username @username end |
Instance Method Details
#folders ⇒ Object
List all IMAP folders
187 188 189 190 191 192 193 194 195 |
# File 'lib/tina4/messenger.rb', line 187 def folders imap_connect do |imap| boxes = imap.list("", "*") (boxes || []).map(&:name) end rescue => e Tina4::Log.error("IMAP folders failed: #{e.}") [] end |
#inbox(folder: "INBOX", limit: 20, offset: 0) ⇒ Object
List messages in a folder
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/tina4/messenger.rb', line 117 def inbox(folder: "INBOX", limit: 20, offset: 0) imap_connect do |imap| imap.select(folder) uids = imap.uid_search(["ALL"]) uids = uids.reverse # newest first page = uids[offset, limit] || [] return [] if page.empty? envelopes = imap.uid_fetch(page, ["ENVELOPE", "FLAGS", "RFC822.SIZE"]) (envelopes || []).map { |msg| parse_envelope(msg) } end rescue => e Tina4::Log.error("IMAP inbox failed: #{e.}") [] end |
#mark_read(uid, folder: "INBOX") ⇒ Object
Mark a message as read (set Seen flag).
201 202 203 204 205 206 207 208 |
# File 'lib/tina4/messenger.rb', line 201 def mark_read(uid, folder: "INBOX") imap_connect do |imap| imap.select(folder) imap.uid_store(uid.to_i, "+FLAGS", [:Seen]) end rescue => e Tina4::Log.error("IMAP mark_read failed: #{e.}") end |
#read(uid, folder: "INBOX", mark_read: true) ⇒ Object
Read a single message by UID
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/tina4/messenger.rb', line 134 def read(uid, folder: "INBOX", mark_read: true) imap_connect do |imap| imap.select(folder) data = imap.uid_fetch(uid, ["ENVELOPE", "FLAGS", "BODY[]", "RFC822.SIZE"]) return nil if data.nil? || data.empty? if mark_read imap.uid_store(uid, "+FLAGS", [:Seen]) end msg = data.first (msg) end rescue => e Tina4::Log.error("IMAP read failed: #{e.}") nil end |
#search(folder: "INBOX", subject: nil, sender: nil, since: nil, before: nil, unseen_only: false, limit: 20) ⇒ Object
Search messages with filters
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/tina4/messenger.rb', line 165 def search(folder: "INBOX", subject: nil, sender: nil, since: nil, before: nil, unseen_only: false, limit: 20) imap_connect do |imap| imap.select(folder) criteria = build_search_criteria( subject: subject, sender: sender, since: since, before: before, unseen_only: unseen_only ) uids = imap.uid_search(criteria) uids = uids.reverse page = uids[0, limit] || [] return [] if page.empty? envelopes = imap.uid_fetch(page, ["ENVELOPE", "FLAGS", "RFC822.SIZE"]) (envelopes || []).map { |msg| parse_envelope(msg) } end rescue => e Tina4::Log.error("IMAP search failed: #{e.}") [] end |
#send(to:, subject:, body:, html: false, text: nil, cc: [], bcc: [], reply_to: nil, attachments: [], headers: {}) ⇒ Object
Send email using Ruby’s Net::SMTP Returns { success: true/false, message: “…”, id: “…” }
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/tina4/messenger.rb', line 73 def send(to:, subject:, body:, html: false, text: nil, cc: [], bcc: [], reply_to: nil, attachments: [], headers: {}) = "<#{SecureRandom.uuid}@#{@host}>" raw = ( to: to, subject: subject, body: body, html: html, text: text, cc: cc, bcc: bcc, reply_to: reply_to, attachments: , headers: headers, message_id: ) all_recipients = normalize_recipients(to) + normalize_recipients(cc) + normalize_recipients(bcc) smtp = Net::SMTP.new(@host, @port) smtp.enable_starttls if @use_tls smtp.start(@host, @username, @password, auth_method) do |conn| conn.(raw, @from_address, all_recipients) end Tina4::Log.info("Email sent to #{Array(to).join(', ')}: #{subject}") { success: true, message: "Email sent successfully", id: } rescue => e Tina4::Log.error("Email send failed: #{e.}") { success: false, message: e., id: nil } end |
#test_connection ⇒ Object
Test SMTP connection Returns { success: true/false, message: “…” }
103 104 105 106 107 108 109 110 111 112 |
# File 'lib/tina4/messenger.rb', line 103 def test_connection smtp = Net::SMTP.new(@host, @port) smtp.enable_starttls if @use_tls smtp.start(@host, @username, @password, auth_method) do |_conn| # connection succeeded end { success: true, message: "SMTP connection successful" } rescue => e { success: false, message: e. } end |
#test_imap_connection ⇒ Hash
Test IMAP connectivity without reading messages.
213 214 215 216 217 218 219 220 |
# File 'lib/tina4/messenger.rb', line 213 def test_imap_connection imap_connect do |_imap| # Connection succeeded end { success: true, message: "Connected to #{@imap_host}:#{@imap_port}" } rescue => e { success: false, message: "IMAP connection failed: #{e.}" } end |
#unread(folder: "INBOX") ⇒ Object
Count unread messages
153 154 155 156 157 158 159 160 161 162 |
# File 'lib/tina4/messenger.rb', line 153 def unread(folder: "INBOX") imap_connect do |imap| imap.select(folder) uids = imap.uid_search(["UNSEEN"]) uids.length end rescue => e Tina4::Log.error("IMAP unread count failed: #{e.}") 0 end |