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_* 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

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) ⇒ 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

#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_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

#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



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



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



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.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: “…” }



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: {})
  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: “…” }



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

#test_imap_connectionHash

Test IMAP connectivity without reading messages.

Returns:

  • (Hash)

    { success: Boolean, message: String }



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