heathrow 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +58 -0
- data/README.md +205 -0
- data/bin/heathrow +42 -0
- data/bin/heathrowd +283 -0
- data/docs/ARCHITECTURE.md +1172 -0
- data/docs/DATABASE_SCHEMA.md +685 -0
- data/docs/DEVELOPMENT_WORKFLOW.md +867 -0
- data/docs/DISCORD_SETUP.md +142 -0
- data/docs/GMAIL_OAUTH_SETUP.md +120 -0
- data/docs/PLUGIN_SYSTEM.md +1370 -0
- data/docs/PROJECT_PLAN.md +1022 -0
- data/docs/README.md +417 -0
- data/docs/REDDIT_SETUP.md +174 -0
- data/docs/REPLY_FORWARD.md +182 -0
- data/docs/WHATSAPP_TELEGRAM_SETUP.md +306 -0
- data/heathrow.gemspec +34 -0
- data/heathrowd.service +21 -0
- data/img/heathrow.svg +95 -0
- data/img/rss_threaded.png +0 -0
- data/img/sources.png +0 -0
- data/lib/heathrow/address_book.rb +42 -0
- data/lib/heathrow/config.rb +332 -0
- data/lib/heathrow/database.rb +731 -0
- data/lib/heathrow/database_new.rb +392 -0
- data/lib/heathrow/event_bus.rb +175 -0
- data/lib/heathrow/logger.rb +122 -0
- data/lib/heathrow/message.rb +176 -0
- data/lib/heathrow/message_composer.rb +399 -0
- data/lib/heathrow/message_organizer.rb +774 -0
- data/lib/heathrow/migrations/001_initial_schema.rb +248 -0
- data/lib/heathrow/notmuch.rb +45 -0
- data/lib/heathrow/oauth2_smtp.rb +254 -0
- data/lib/heathrow/plugin/base.rb +212 -0
- data/lib/heathrow/plugin_manager.rb +141 -0
- data/lib/heathrow/poller.rb +93 -0
- data/lib/heathrow/smtp_sender.rb +204 -0
- data/lib/heathrow/source.rb +39 -0
- data/lib/heathrow/sources/base.rb +74 -0
- data/lib/heathrow/sources/discord.rb +357 -0
- data/lib/heathrow/sources/gmail.rb +294 -0
- data/lib/heathrow/sources/imap.rb +198 -0
- data/lib/heathrow/sources/instagram.rb +307 -0
- data/lib/heathrow/sources/instagram_fetch.py +101 -0
- data/lib/heathrow/sources/instagram_send.py +55 -0
- data/lib/heathrow/sources/instagram_send_marionette.py +104 -0
- data/lib/heathrow/sources/maildir.rb +606 -0
- data/lib/heathrow/sources/messenger.rb +212 -0
- data/lib/heathrow/sources/messenger_fetch.js +297 -0
- data/lib/heathrow/sources/messenger_fetch_marionette.py +138 -0
- data/lib/heathrow/sources/messenger_send.js +32 -0
- data/lib/heathrow/sources/messenger_send.py +100 -0
- data/lib/heathrow/sources/reddit.rb +461 -0
- data/lib/heathrow/sources/rss.rb +299 -0
- data/lib/heathrow/sources/slack.rb +375 -0
- data/lib/heathrow/sources/source_manager.rb +328 -0
- data/lib/heathrow/sources/telegram.rb +498 -0
- data/lib/heathrow/sources/webpage.rb +207 -0
- data/lib/heathrow/sources/weechat.rb +479 -0
- data/lib/heathrow/sources/whatsapp.rb +474 -0
- data/lib/heathrow/ui/application.rb +8098 -0
- data/lib/heathrow/ui/navigation.rb +8 -0
- data/lib/heathrow/ui/panes.rb +8 -0
- data/lib/heathrow/ui/source_wizard.rb +567 -0
- data/lib/heathrow/ui/threaded_view.rb +780 -0
- data/lib/heathrow/ui/views.rb +8 -0
- data/lib/heathrow/version.rb +3 -0
- data/lib/heathrow/wizards/discord_wizard.rb +193 -0
- data/lib/heathrow/wizards/slack_wizard.rb +140 -0
- data/lib/heathrow.rb +55 -0
- metadata +147 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'net/imap'
|
|
5
|
+
require 'gmail_xoauth'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require 'open3'
|
|
9
|
+
|
|
10
|
+
module Heathrow
|
|
11
|
+
module Sources
|
|
12
|
+
class Gmail
|
|
13
|
+
def initialize(source)
|
|
14
|
+
@source = source
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def name
|
|
18
|
+
'Gmail'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def description
|
|
22
|
+
'Fetch emails from Gmail using OAuth2'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def config_fields
|
|
26
|
+
[
|
|
27
|
+
{ name: 'email', label: 'Gmail Address', type: 'text', required: true },
|
|
28
|
+
{ name: 'safedir', label: 'Safe Directory', type: 'text', required: true,
|
|
29
|
+
hint: 'Directory containing your .json and .txt OAuth files' },
|
|
30
|
+
{ name: 'oauth2_script', label: 'OAuth2 Script Path', type: 'text', required: true,
|
|
31
|
+
hint: 'Path to oauth2.py script' },
|
|
32
|
+
{ name: 'folder', label: 'Folder to Monitor', type: 'text', default: 'INBOX' },
|
|
33
|
+
{ name: 'fetch_limit', label: 'Max messages per fetch', type: 'number', default: 50 },
|
|
34
|
+
{ name: 'check_interval', label: 'Check Interval (seconds)', type: 'number', default: 300 },
|
|
35
|
+
{ name: 'mark_as_read', label: 'Mark fetched as read', type: 'boolean', default: false,
|
|
36
|
+
hint: 'CAUTION: Only enable if you want Heathrow to mark emails as read' }
|
|
37
|
+
]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate_config(config)
|
|
41
|
+
return { valid: false, error: 'Email is required' } unless config['email']
|
|
42
|
+
return { valid: false, error: 'Safe directory is required' } unless config['safedir']
|
|
43
|
+
|
|
44
|
+
# Check if OAuth files exist
|
|
45
|
+
email = config['email']
|
|
46
|
+
safedir = File.expand_path(config['safedir'])
|
|
47
|
+
json_file = File.join(safedir, "#{email}.json")
|
|
48
|
+
txt_file = File.join(safedir, "#{email}.txt")
|
|
49
|
+
|
|
50
|
+
unless File.exist?(json_file)
|
|
51
|
+
return { valid: false, error: "OAuth JSON file not found: #{json_file}" }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
unless File.exist?(txt_file)
|
|
55
|
+
return { valid: false, error: "Refresh token file not found: #{txt_file}" }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
{ valid: true }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def fetch_messages
|
|
62
|
+
email = @source.config['email']
|
|
63
|
+
safedir = File.expand_path(@source.config['safedir'])
|
|
64
|
+
oauth2_script = File.expand_path(@source.config['oauth2_script'] || '~/bin/oauth2.py')
|
|
65
|
+
folder = @source.config['folder'] || 'INBOX'
|
|
66
|
+
fetch_limit = @source.config['fetch_limit'] || 50
|
|
67
|
+
mark_as_read = @source.config['mark_as_read'] || false
|
|
68
|
+
|
|
69
|
+
# Read OAuth credentials
|
|
70
|
+
json_file = File.join(safedir, "#{email}.json")
|
|
71
|
+
txt_file = File.join(safedir, "#{email}.txt")
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
jparse = JSON.parse(File.read(json_file))
|
|
75
|
+
clientid = jparse["web"]["client_id"]
|
|
76
|
+
clsecret = jparse["web"]["client_secret"]
|
|
77
|
+
refresh_token = File.read(txt_file).chomp
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
return [{
|
|
80
|
+
source_id: @source.id,
|
|
81
|
+
source_type: 'gmail',
|
|
82
|
+
sender: 'Gmail',
|
|
83
|
+
subject: 'Configuration Error',
|
|
84
|
+
content: "Failed to read OAuth credentials: #{e.message}",
|
|
85
|
+
timestamp: Time.now.to_s,
|
|
86
|
+
is_read: 0
|
|
87
|
+
}]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get access token using oauth2.py
|
|
91
|
+
begin
|
|
92
|
+
cmd = "python3 #{oauth2_script} --generate_oauth2_token " \
|
|
93
|
+
"--client_id=#{clientid} --client_secret=#{clsecret} " \
|
|
94
|
+
"--refresh_token=#{refresh_token}"
|
|
95
|
+
|
|
96
|
+
require 'timeout'
|
|
97
|
+
stdout = stderr = nil
|
|
98
|
+
status = nil
|
|
99
|
+
|
|
100
|
+
Timeout::timeout(10) do
|
|
101
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
unless status && status.success?
|
|
105
|
+
raise "OAuth token generation failed: #{stderr || 'No output'}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Extract access token from output (format: "Access Token: <token>")
|
|
109
|
+
token_match = stdout.match(/Access Token: (.+?)(\n|$)/)
|
|
110
|
+
unless token_match
|
|
111
|
+
raise "Could not extract access token from output: #{stdout}"
|
|
112
|
+
end
|
|
113
|
+
token = token_match[1].strip
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
return [{
|
|
116
|
+
source_id: @source.id,
|
|
117
|
+
source_type: 'gmail',
|
|
118
|
+
sender: 'Gmail',
|
|
119
|
+
subject: 'Authentication Error',
|
|
120
|
+
content: "Failed to get access token: #{e.message}",
|
|
121
|
+
timestamp: Time.now.to_s,
|
|
122
|
+
is_read: 0
|
|
123
|
+
}]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
messages = []
|
|
127
|
+
|
|
128
|
+
begin
|
|
129
|
+
# Connect to Gmail IMAP
|
|
130
|
+
imap = Net::IMAP.new('imap.gmail.com', 993, usessl = true, certs = nil, verify = false)
|
|
131
|
+
|
|
132
|
+
# Authenticate with OAuth2
|
|
133
|
+
imap.authenticate('XOAUTH2', email, token)
|
|
134
|
+
imap.select(folder)
|
|
135
|
+
|
|
136
|
+
# Get state file to track which messages we've already seen
|
|
137
|
+
heathrow_home = File.expand_path('~/.heathrow')
|
|
138
|
+
state_file = File.join(heathrow_home, 'state', "gmail_#{@source.id}.json")
|
|
139
|
+
FileUtils.mkdir_p(File.dirname(state_file))
|
|
140
|
+
|
|
141
|
+
seen_uids = if File.exist?(state_file)
|
|
142
|
+
JSON.parse(File.read(state_file))['seen_uids'] || []
|
|
143
|
+
else
|
|
144
|
+
[]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Search for messages (both seen and unseen for testing)
|
|
148
|
+
# In production, you might want to use ["UNSEEN"] only
|
|
149
|
+
search_criteria = mark_as_read ? ["UNSEEN"] : ["ALL"]
|
|
150
|
+
|
|
151
|
+
# Get UIDs of messages
|
|
152
|
+
uids = imap.uid_search(search_criteria)
|
|
153
|
+
|
|
154
|
+
# Limit the number of messages to fetch
|
|
155
|
+
new_uids = uids.reject { |uid| seen_uids.include?(uid) }.last(fetch_limit)
|
|
156
|
+
|
|
157
|
+
new_uids.each do |uid|
|
|
158
|
+
begin
|
|
159
|
+
# Fetch the message envelope and body
|
|
160
|
+
fetch_data = imap.uid_fetch(uid, ['ENVELOPE', 'BODY.PEEK[TEXT]', 'FLAGS'])[0]
|
|
161
|
+
|
|
162
|
+
envelope = fetch_data.attr['ENVELOPE']
|
|
163
|
+
body = fetch_data.attr['BODY[TEXT]'] || ''
|
|
164
|
+
flags = fetch_data.attr['FLAGS'] || []
|
|
165
|
+
|
|
166
|
+
# Extract sender
|
|
167
|
+
from = if envelope.from && envelope.from.first
|
|
168
|
+
addr = envelope.from.first
|
|
169
|
+
if addr.name
|
|
170
|
+
"#{addr.name} <#{addr.mailbox}@#{addr.host}>"
|
|
171
|
+
else
|
|
172
|
+
"#{addr.mailbox}@#{addr.host}"
|
|
173
|
+
end
|
|
174
|
+
else
|
|
175
|
+
'Unknown Sender'
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Clean up subject
|
|
179
|
+
subject = envelope.subject || '(No Subject)'
|
|
180
|
+
|
|
181
|
+
# Parse date
|
|
182
|
+
date = envelope.date ? Time.parse(envelope.date) : Time.now
|
|
183
|
+
|
|
184
|
+
# Truncate body for preview
|
|
185
|
+
body_preview = body.to_s
|
|
186
|
+
.force_encoding('UTF-8')
|
|
187
|
+
.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
188
|
+
.gsub(/\r\n/, "\n")
|
|
189
|
+
.strip[0..500]
|
|
190
|
+
|
|
191
|
+
# Check if message is unread
|
|
192
|
+
is_unread = !flags.include?(:Seen)
|
|
193
|
+
|
|
194
|
+
messages << {
|
|
195
|
+
source_id: @source.id,
|
|
196
|
+
source_type: 'gmail',
|
|
197
|
+
sender: from,
|
|
198
|
+
subject: subject,
|
|
199
|
+
content: body_preview,
|
|
200
|
+
timestamp: date.to_s,
|
|
201
|
+
is_read: is_unread ? 0 : 1,
|
|
202
|
+
metadata: {
|
|
203
|
+
uid: uid,
|
|
204
|
+
folder: folder,
|
|
205
|
+
message_id: envelope.message_id,
|
|
206
|
+
flags: flags.map(&:to_s)
|
|
207
|
+
}.to_json
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Add to seen UIDs list
|
|
211
|
+
seen_uids << uid
|
|
212
|
+
|
|
213
|
+
# IMPORTANT: Only mark as read if explicitly configured
|
|
214
|
+
if mark_as_read && is_unread
|
|
215
|
+
imap.uid_store(uid, "+FLAGS", [:Seen])
|
|
216
|
+
end
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
# Log error but continue with other messages
|
|
219
|
+
puts "Error fetching message UID #{uid}: #{e.message}" if ENV['DEBUG']
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Save state
|
|
224
|
+
File.write(state_file, JSON.pretty_generate({
|
|
225
|
+
seen_uids: seen_uids,
|
|
226
|
+
last_check: Time.now.to_s
|
|
227
|
+
}))
|
|
228
|
+
|
|
229
|
+
# Disconnect
|
|
230
|
+
imap.logout
|
|
231
|
+
imap.disconnect
|
|
232
|
+
|
|
233
|
+
rescue StandardError => e
|
|
234
|
+
return [{
|
|
235
|
+
source_id: @source.id,
|
|
236
|
+
source_type: 'gmail',
|
|
237
|
+
sender: 'Gmail',
|
|
238
|
+
subject: 'Connection Error',
|
|
239
|
+
content: "Failed to connect to Gmail: #{e.message}",
|
|
240
|
+
timestamp: Time.now.to_s,
|
|
241
|
+
is_read: 0
|
|
242
|
+
}]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
messages
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def test
|
|
249
|
+
# Test connection and authentication
|
|
250
|
+
email = @source.config['email']
|
|
251
|
+
|
|
252
|
+
begin
|
|
253
|
+
# Try to get token
|
|
254
|
+
messages = fetch_messages
|
|
255
|
+
|
|
256
|
+
if messages.any? { |m| m[:subject] =~ /Error/ }
|
|
257
|
+
{ success: false, message: messages.first[:content] }
|
|
258
|
+
else
|
|
259
|
+
{
|
|
260
|
+
success: true,
|
|
261
|
+
message: "Successfully connected to Gmail for #{email}. Found #{messages.size} messages."
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
{ success: false, message: "Test failed: #{e.message}" }
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def can_reply?
|
|
270
|
+
true
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def send_message(to, subject, body, in_reply_to = nil)
|
|
274
|
+
config = @source.config.is_a?(String) ? JSON.parse(@source.config) : @source.config
|
|
275
|
+
email = config['email']
|
|
276
|
+
|
|
277
|
+
# Use the SmtpSender module
|
|
278
|
+
require_relative '../smtp_sender'
|
|
279
|
+
|
|
280
|
+
# SmtpSender handles all the complexity of OAuth2 and gmail_smtp
|
|
281
|
+
result = Heathrow::SmtpSender.send_message(
|
|
282
|
+
email,
|
|
283
|
+
to,
|
|
284
|
+
subject,
|
|
285
|
+
body,
|
|
286
|
+
in_reply_to,
|
|
287
|
+
config
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
result
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'net/imap'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
|
|
8
|
+
module Heathrow
|
|
9
|
+
module Sources
|
|
10
|
+
class Imap
|
|
11
|
+
def initialize(source)
|
|
12
|
+
@source = source
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def name
|
|
16
|
+
'IMAP Email'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def description
|
|
20
|
+
'Fetch emails from any IMAP server using username/password'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fetch_messages
|
|
24
|
+
server = @source.config['imap_server']
|
|
25
|
+
port = @source.config['imap_port'] || 993
|
|
26
|
+
username = @source.config['username']
|
|
27
|
+
password = @source.config['password']
|
|
28
|
+
folder = @source.config['folder'] || 'INBOX'
|
|
29
|
+
fetch_limit = @source.config['fetch_limit'] || 50
|
|
30
|
+
mark_as_read = @source.config['mark_as_read'] || false
|
|
31
|
+
use_ssl = @source.config['use_ssl'] != false # Default to true
|
|
32
|
+
|
|
33
|
+
messages = []
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
# Connect to IMAP server
|
|
37
|
+
if use_ssl
|
|
38
|
+
imap = Net::IMAP.new(server, port, usessl = true, certs = nil, verify = false)
|
|
39
|
+
else
|
|
40
|
+
imap = Net::IMAP.new(server, port)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Login with username/password
|
|
44
|
+
imap.login(username, password)
|
|
45
|
+
imap.select(folder)
|
|
46
|
+
|
|
47
|
+
# Get state file to track which messages we've already seen
|
|
48
|
+
heathrow_home = File.expand_path('~/.heathrow')
|
|
49
|
+
state_file = File.join(heathrow_home, 'state', "imap_#{@source.id}.json")
|
|
50
|
+
FileUtils.mkdir_p(File.dirname(state_file))
|
|
51
|
+
|
|
52
|
+
seen_uids = if File.exist?(state_file)
|
|
53
|
+
JSON.parse(File.read(state_file))['seen_uids'] || []
|
|
54
|
+
else
|
|
55
|
+
[]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Search for unseen messages
|
|
59
|
+
unseen_uids = imap.search(["UNSEEN"])
|
|
60
|
+
|
|
61
|
+
# Get UIDs not already processed
|
|
62
|
+
new_uids = unseen_uids - seen_uids
|
|
63
|
+
new_uids = new_uids.first(fetch_limit) if new_uids.length > fetch_limit
|
|
64
|
+
|
|
65
|
+
new_uids.each do |uid|
|
|
66
|
+
msg = imap.uid_fetch(uid, ["ENVELOPE", "BODY[TEXT]", "FLAGS"]).first
|
|
67
|
+
next unless msg
|
|
68
|
+
|
|
69
|
+
envelope = msg.attr["ENVELOPE"]
|
|
70
|
+
body = msg.attr["BODY[TEXT]"]
|
|
71
|
+
flags = msg.attr["FLAGS"]
|
|
72
|
+
|
|
73
|
+
from = envelope.from&.first
|
|
74
|
+
sender = from ? "#{from.name || ''} <#{from.mailbox}@#{from.host}>" : "Unknown"
|
|
75
|
+
|
|
76
|
+
is_unread = !flags.include?(:Seen)
|
|
77
|
+
|
|
78
|
+
# Mark as read if configured to do so
|
|
79
|
+
if mark_as_read && is_unread
|
|
80
|
+
imap.uid_store(uid, "+FLAGS", [:Seen])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
message = {
|
|
84
|
+
source_id: @source.id,
|
|
85
|
+
source_type: 'imap',
|
|
86
|
+
external_id: "imap_#{username}_#{uid}",
|
|
87
|
+
sender: sender,
|
|
88
|
+
recipient: username,
|
|
89
|
+
subject: envelope.subject || "(no subject)",
|
|
90
|
+
content: body || "",
|
|
91
|
+
raw_data: {
|
|
92
|
+
envelope: envelope,
|
|
93
|
+
flags: flags,
|
|
94
|
+
uid: uid
|
|
95
|
+
}.to_json,
|
|
96
|
+
attachments: nil,
|
|
97
|
+
timestamp: envelope.date || Time.now,
|
|
98
|
+
is_read: flags.include?(:Seen) ? 1 : 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
messages << message
|
|
102
|
+
seen_uids << uid
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Update state file
|
|
106
|
+
File.write(state_file, JSON.generate({
|
|
107
|
+
seen_uids: seen_uids,
|
|
108
|
+
last_fetch: Time.now.to_s
|
|
109
|
+
}))
|
|
110
|
+
|
|
111
|
+
imap.logout
|
|
112
|
+
imap.disconnect
|
|
113
|
+
rescue Net::IMAP::NoResponseError => e
|
|
114
|
+
# Authentication failed
|
|
115
|
+
return [{
|
|
116
|
+
source_id: @source.id,
|
|
117
|
+
source_type: 'imap',
|
|
118
|
+
sender: 'IMAP',
|
|
119
|
+
subject: 'Authentication Failed',
|
|
120
|
+
content: "Failed to login to #{server}: #{e.message}",
|
|
121
|
+
timestamp: Time.now.to_s,
|
|
122
|
+
is_read: 0
|
|
123
|
+
}]
|
|
124
|
+
rescue => e
|
|
125
|
+
# Other errors
|
|
126
|
+
return [{
|
|
127
|
+
source_id: @source.id,
|
|
128
|
+
source_type: 'imap',
|
|
129
|
+
sender: 'IMAP',
|
|
130
|
+
subject: 'Connection Error',
|
|
131
|
+
content: "Error connecting to #{server}: #{e.message}",
|
|
132
|
+
timestamp: Time.now.to_s,
|
|
133
|
+
is_read: 0
|
|
134
|
+
}]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
messages
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def test_connection
|
|
141
|
+
config = @source.config.is_a?(String) ? JSON.parse(@source.config) : @source.config
|
|
142
|
+
server = config['imap_server']
|
|
143
|
+
port = config['imap_port'] || 993
|
|
144
|
+
username = config['username']
|
|
145
|
+
password = config['password']
|
|
146
|
+
use_ssl = config['use_ssl'] != false
|
|
147
|
+
|
|
148
|
+
begin
|
|
149
|
+
if use_ssl
|
|
150
|
+
imap = Net::IMAP.new(server, port, usessl = true, certs = nil, verify = false)
|
|
151
|
+
else
|
|
152
|
+
imap = Net::IMAP.new(server, port)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
imap.login(username, password)
|
|
156
|
+
|
|
157
|
+
# Get folder list to verify connection
|
|
158
|
+
folders = imap.list('', '*')
|
|
159
|
+
folder_count = folders&.size || 0
|
|
160
|
+
|
|
161
|
+
imap.logout
|
|
162
|
+
imap.disconnect
|
|
163
|
+
|
|
164
|
+
{ success: true, message: "Connected to #{server} (#{folder_count} folders)" }
|
|
165
|
+
rescue => e
|
|
166
|
+
{ success: false, message: "Connection failed: #{e.message}" }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def can_reply?
|
|
171
|
+
config = @source.config.is_a?(String) ? JSON.parse(@source.config) : @source.config
|
|
172
|
+
# Check if we have SMTP configuration or can use gmail_smtp
|
|
173
|
+
config['smtp_server'] || config['username']&.include?('@')
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def send_message(to, subject, body, in_reply_to = nil)
|
|
177
|
+
config = @source.config.is_a?(String) ? JSON.parse(@source.config) : @source.config
|
|
178
|
+
from = config['username']
|
|
179
|
+
|
|
180
|
+
# Use the SmtpSender module
|
|
181
|
+
require_relative '../smtp_sender'
|
|
182
|
+
|
|
183
|
+
# SmtpSender will automatically detect OAuth2 domains and use gmail_smtp
|
|
184
|
+
# or fall back to SMTP server configuration
|
|
185
|
+
result = Heathrow::SmtpSender.send_message(
|
|
186
|
+
from,
|
|
187
|
+
to,
|
|
188
|
+
subject,
|
|
189
|
+
body,
|
|
190
|
+
in_reply_to,
|
|
191
|
+
config
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
result
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|