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_*) > 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_encryption ⇒ Object
readonly
Returns the value of attribute imap_encryption.
-
#imap_host ⇒ Object
readonly
Returns the value of attribute imap_host.
-
#imap_port ⇒ Object
readonly
Returns the value of attribute imap_port.
-
#imap_use_tls ⇒ Object
readonly
Returns the value of attribute imap_use_tls.
-
#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, imap_encryption: 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, imap_encryption: nil) ⇒ Messenger
Initialize with SMTP config. Priority: constructor params > ENV (TINA4_MAIL_*) > sensible defaults
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/tina4/messenger.rb', line 80 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, imap_encryption: nil) @host = host || ENV["TINA4_MAIL_HOST"] || "localhost" @port = (port || ENV["TINA4_MAIL_PORT"] || 587).to_i @username = username || ENV["TINA4_MAIL_USERNAME"] @password = password || ENV["TINA4_MAIL_PASSWORD"] resolved_from = from_address || ENV["TINA4_MAIL_FROM"] @from_address = resolved_from || @username || "noreply@localhost" @from_name = from_name || ENV["TINA4_MAIL_FROM_NAME"] || "" # SMTP 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"] || @host @imap_port = (imap_port || ENV["TINA4_MAIL_IMAP_PORT"] || 993).to_i # IMAP encryption: dedicated env var TINA4_MAIL_IMAP_ENCRYPTION (tls/starttls/none). # Defaults to "tls" — IMAPS over implicit TLS on port 993 is the safe industry norm. env_imap_enc = imap_encryption || ENV["TINA4_MAIL_IMAP_ENCRYPTION"] @imap_encryption = (env_imap_enc && !env_imap_enc.to_s.empty?) ? env_imap_enc.to_s.downcase : "tls" @imap_use_tls = %w[tls starttls ssl].include?(@imap_encryption) end |
Instance Attribute Details
#encryption ⇒ Object (readonly)
Returns the value of attribute encryption.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def encryption @encryption end |
#from_address ⇒ Object (readonly)
Returns the value of attribute from_address.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def from_address @from_address end |
#from_name ⇒ Object (readonly)
Returns the value of attribute from_name.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def from_name @from_name end |
#host ⇒ Object (readonly)
Returns the value of attribute host.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def host @host end |
#imap_encryption ⇒ Object (readonly)
Returns the value of attribute imap_encryption.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def imap_encryption @imap_encryption end |
#imap_host ⇒ Object (readonly)
Returns the value of attribute imap_host.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def imap_host @imap_host end |
#imap_port ⇒ Object (readonly)
Returns the value of attribute imap_port.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def imap_port @imap_port end |
#imap_use_tls ⇒ Object (readonly)
Returns the value of attribute imap_use_tls.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def imap_use_tls @imap_use_tls end |
#port ⇒ Object (readonly)
Returns the value of attribute port.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def port @port end |
#use_tls ⇒ Object (readonly)
Returns the value of attribute use_tls.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def use_tls @use_tls end |
#username ⇒ Object (readonly)
Returns the value of attribute username.
74 75 76 |
# File 'lib/tina4/messenger.rb', line 74 def username @username end |
Instance Method Details
#folders ⇒ Object
List all IMAP folders.
Raises Tina4::MessengerConnectionError on a connection/protocol failure.
253 254 255 256 257 258 259 260 261 262 263 |
# File 'lib/tina4/messenger.rb', line 253 def folders imap = imap_open("folders") begin boxes = imap.list("", "*") (boxes || []).map(&:name) rescue *IMAP_CONNECTION_ERRORS => e raise imap_fail("folders", e) ensure imap_cleanup(imap) end end |
#inbox(folder: "INBOX", limit: 20, offset: 0) ⇒ Object
List messages in a folder.
Raises Tina4::MessengerConnectionError on a connection/auth/protocol failure (FAILS LOUD — never returns [] to hide it). A successful fetch from an empty folder returns [] (that is NOT an error).
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/tina4/messenger.rb', line 164 def inbox(folder: "INBOX", limit: 20, offset: 0) imap = imap_open("inbox") begin 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) } rescue *IMAP_CONNECTION_ERRORS => e raise imap_fail("inbox", e) ensure imap_cleanup(imap) end end |
#mark_read(uid, folder: "INBOX") ⇒ Object
Mark a message as read (set Seen flag).
269 270 271 272 273 274 275 276 |
# File 'lib/tina4/messenger.rb', line 269 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.
Raises Tina4::MessengerConnectionError on a connection/protocol failure. A successful fetch for a non-existent UID returns nil (that is NOT an error).
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/tina4/messenger.rb', line 186 def read(uid, folder: "INBOX", mark_read: true) imap = imap_open("read") begin 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) rescue *IMAP_CONNECTION_ERRORS => e raise imap_fail("read", e) ensure imap_cleanup(imap) end end |
#search(folder: "INBOX", subject: nil, sender: nil, since: nil, before: nil, unseen_only: false, limit: 20) ⇒ Object
Search messages with filters.
Raises Tina4::MessengerConnectionError on a connection/protocol failure. A successful search with no matches returns [] (NOT an error).
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/tina4/messenger.rb', line 227 def search(folder: "INBOX", subject: nil, sender: nil, since: nil, before: nil, unseen_only: false, limit: 20) imap = imap_open("search") begin 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) } rescue *IMAP_CONNECTION_ERRORS => e raise imap_fail("search", e) ensure imap_cleanup(imap) end 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: “…” }
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/tina4/messenger.rb', line 116 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: “…” }
146 147 148 149 150 151 152 153 154 155 |
# File 'lib/tina4/messenger.rb', line 146 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.
281 282 283 284 285 286 287 288 |
# File 'lib/tina4/messenger.rb', line 281 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.
Raises Tina4::MessengerConnectionError on a connection/protocol failure. A successful query with no unseen messages returns 0 (NOT an error).
210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/tina4/messenger.rb', line 210 def unread(folder: "INBOX") imap = imap_open("unread") begin imap.select(folder) uids = imap.uid_search(["UNSEEN"]) uids.length rescue *IMAP_CONNECTION_ERRORS => e raise imap_fail("unread", e) ensure imap_cleanup(imap) end end |