Class: Tina4::Messenger

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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



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
70
71
72
73
74
75
76
# File 'lib/tina4/messenger.rb', line 44

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

#encryptionObject (readonly)

Returns the value of attribute encryption.



38
39
40
# File 'lib/tina4/messenger.rb', line 38

def encryption
  @encryption
end

#from_addressObject (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_nameObject (readonly)

Returns the value of attribute from_name.



38
39
40
# File 'lib/tina4/messenger.rb', line 38

def from_name
  @from_name
end

#hostObject (readonly)

Returns the value of attribute host.



38
39
40
# File 'lib/tina4/messenger.rb', line 38

def host
  @host
end

#imap_encryptionObject (readonly)

Returns the value of attribute imap_encryption.



38
39
40
# File 'lib/tina4/messenger.rb', line 38

def imap_encryption
  @imap_encryption
end

#imap_hostObject (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_portObject (readonly)

Returns the value of attribute imap_port.



38
39
40
# File 'lib/tina4/messenger.rb', line 38

def imap_port
  @imap_port
end

#imap_use_tlsObject (readonly)

Returns the value of attribute imap_use_tls.



38
39
40
# File 'lib/tina4/messenger.rb', line 38

def imap_use_tls
  @imap_use_tls
end

#portObject (readonly)

Returns the value of attribute port.



38
39
40
# File 'lib/tina4/messenger.rb', line 38

def port
  @port
end

#use_tlsObject (readonly)

Returns the value of attribute use_tls.



38
39
40
# File 'lib/tina4/messenger.rb', line 38

def use_tls
  @use_tls
end

#usernameObject (readonly)

Returns the value of attribute username.



38
39
40
# File 'lib/tina4/messenger.rb', line 38

def username
  @username
end

Instance Method Details

#foldersObject

List all IMAP folders



194
195
196
197
198
199
200
201
202
# File 'lib/tina4/messenger.rb', line 194

def folders
  imap_connect do |imap|
    boxes = imap.list("", "*")
    (boxes || []).map(&:name)
  end
rescue => e
  Tina4::Log.error("IMAP folders failed: #{e.message}")
  []
end

#inbox(folder: "INBOX", limit: 20, offset: 0) ⇒ Object

List messages in a folder



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/tina4/messenger.rb', line 124

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.message}")
  []
end

#mark_read(uid, folder: "INBOX") ⇒ Object

Mark a message as read (set Seen flag).

Parameters:

  • uid (String, Integer)

    message UID

  • folder (String) (defaults to: "INBOX")

    IMAP folder name



208
209
210
211
212
213
214
215
# File 'lib/tina4/messenger.rb', line 208

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.message}")
end

#read(uid, folder: "INBOX", mark_read: true) ⇒ Object

Read a single message by UID



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/tina4/messenger.rb', line 141

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
    parse_full_message(msg)
  end
rescue => e
  Tina4::Log.error("IMAP read failed: #{e.message}")
  nil
end

#search(folder: "INBOX", subject: nil, sender: nil, since: nil, before: nil, unseen_only: false, limit: 20) ⇒ Object

Search messages with filters



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/tina4/messenger.rb', line 172

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.message}")
  []
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: “…” }



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
# File 'lib/tina4/messenger.rb', line 80

def send(to:, subject:, body:, html: false, text: nil, cc: [], bcc: [],
         reply_to: nil, attachments: [], headers: {})
  message_id = "<#{SecureRandom.uuid}@#{@host}>"
  raw = build_message(
    to: to, subject: subject, body: body, html: html, text: text,
    cc: cc, bcc: bcc, reply_to: reply_to,
    attachments: attachments, headers: headers,
    message_id: 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.send_message(raw, @from_address, all_recipients)
  end

  Tina4::Log.info("Email sent to #{Array(to).join(', ')}: #{subject}")
  { success: true, message: "Email sent successfully", id: message_id }
rescue => e
  Tina4::Log.error("Email send failed: #{e.message}")
  { success: false, message: e.message, id: nil }
end

#test_connectionObject

Test SMTP connection Returns { success: true/false, message: “…” }



110
111
112
113
114
115
116
117
118
119
# File 'lib/tina4/messenger.rb', line 110

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.message }
end

#test_imap_connectionHash

Test IMAP connectivity without reading messages.

Returns:

  • (Hash)

    { success: Boolean, message: String }



220
221
222
223
224
225
226
227
# File 'lib/tina4/messenger.rb', line 220

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.message}" }
end

#unread(folder: "INBOX") ⇒ Object

Count unread messages



160
161
162
163
164
165
166
167
168
169
# File 'lib/tina4/messenger.rb', line 160

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.message}")
  0
end