heathrow 0.7.0 → 0.7.1
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 +4 -4
- data/.gitignore +12 -3
- data/lib/heathrow/message_composer.rb +2 -1
- data/lib/heathrow/ui/application.rb +82 -16
- data/lib/heathrow/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c40dfa3681941910b0e8ac358021003220ee9e88b9861b1c20b735bb0444623
|
|
4
|
+
data.tar.gz: f2acadd2ea11c58fa522db1abf94ebdb31a20fd0fc346eac94aefac38a9fe847
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '084222a43672e22966f1716b62a59d7d5961a5148c8a47f858c08cb3a33d863fb1933c74649792ae4a8945fb0a36bbbb6fcbfbe46bdb1e9ed045874708530f95'
|
|
7
|
+
data.tar.gz: 31b2b552ea905f4cdb463f347cc7855d7f6d7b0b9125e4ba903927b20002e9a87b38754a1b2cedf9c519eb37a9692e805dd0ef66b9c25fc85958e0994f214e81
|
data/.gitignore
CHANGED
|
@@ -18,17 +18,26 @@
|
|
|
18
18
|
/lib/bundler/man/
|
|
19
19
|
.bundle/
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
~/.
|
|
21
|
+
# Heathrow data
|
|
22
|
+
~/.heathrow/
|
|
23
23
|
*.db
|
|
24
24
|
*.db-shm
|
|
25
25
|
*.db-wal
|
|
26
26
|
/logs/
|
|
27
27
|
/attachments/
|
|
28
28
|
|
|
29
|
-
#
|
|
29
|
+
# Secrets and credentials
|
|
30
30
|
.env
|
|
31
31
|
.env.local
|
|
32
|
+
.env.*
|
|
33
|
+
*.pem
|
|
34
|
+
*.key
|
|
35
|
+
*.p12
|
|
36
|
+
*.pfx
|
|
37
|
+
credentials.json
|
|
38
|
+
token.json
|
|
39
|
+
*_secret*
|
|
40
|
+
*_token*
|
|
32
41
|
|
|
33
42
|
# Editor directories
|
|
34
43
|
.vscode/
|
|
@@ -27,7 +27,8 @@ module Heathrow
|
|
|
27
27
|
# Compose a forward of a message
|
|
28
28
|
def compose_forward
|
|
29
29
|
template = build_forward_template
|
|
30
|
-
|
|
30
|
+
# Cursor on "To: " line (line 2)
|
|
31
|
+
content = edit_in_editor(template, cursor_line: 2)
|
|
31
32
|
return nil if content.nil? || content.strip.empty?
|
|
32
33
|
parse_composed_message(content)
|
|
33
34
|
end
|
|
@@ -3702,6 +3702,7 @@ module Heathrow
|
|
|
3702
3702
|
def show_folder_browser
|
|
3703
3703
|
@in_folder_browser = true
|
|
3704
3704
|
@folder_browser_index = 0
|
|
3705
|
+
@panes[:top].bg = @topcolor
|
|
3705
3706
|
@folder_collapsed ||= {}
|
|
3706
3707
|
@folder_count_cache = {} # Fresh counts each time
|
|
3707
3708
|
@browser_favorites = nil # Fresh favorites read
|
|
@@ -3969,6 +3970,7 @@ module Heathrow
|
|
|
3969
3970
|
favorites = get_favorite_folders
|
|
3970
3971
|
@in_folder_browser = true
|
|
3971
3972
|
@folder_browser_index = 0
|
|
3973
|
+
@panes[:top].bg = @topcolor
|
|
3972
3974
|
@folder_count_cache = {} # Fresh counts each time
|
|
3973
3975
|
|
|
3974
3976
|
# Build display from favorites only (no DB queries — counts fetched on select)
|
|
@@ -4250,6 +4252,13 @@ module Heathrow
|
|
|
4250
4252
|
msg['folder'] = dest
|
|
4251
4253
|
msg['metadata'] = metadata
|
|
4252
4254
|
msg['labels'] = labels
|
|
4255
|
+
|
|
4256
|
+
# Mark as read (we've seen it if we're filing it)
|
|
4257
|
+
if msg['is_read'].to_i == 0
|
|
4258
|
+
@db.mark_as_read(msg['id'])
|
|
4259
|
+
msg['is_read'] = 1
|
|
4260
|
+
sync_maildir_flag(msg, 'S', true)
|
|
4261
|
+
end
|
|
4253
4262
|
end
|
|
4254
4263
|
|
|
4255
4264
|
# ── Save by browsing folders (sB) ──
|
|
@@ -4517,7 +4526,7 @@ module Heathrow
|
|
|
4517
4526
|
def ai_assistant
|
|
4518
4527
|
msg = current_message
|
|
4519
4528
|
return unless msg && !msg['is_header'] && !msg['is_channel_header'] && !msg['is_thread_header']
|
|
4520
|
-
ensure_full_message(msg)
|
|
4529
|
+
msg = ensure_full_message(msg)
|
|
4521
4530
|
|
|
4522
4531
|
# Build message context
|
|
4523
4532
|
context = ai_message_context(msg)
|
|
@@ -4963,9 +4972,19 @@ module Heathrow
|
|
|
4963
4972
|
end
|
|
4964
4973
|
|
|
4965
4974
|
def ensure_full_message(msg)
|
|
4966
|
-
if msg && msg['id'] && !msg
|
|
4975
|
+
if msg && msg['id'] && !msg['_full_loaded'] && !msg['is_header']
|
|
4967
4976
|
full = @db.get_message(msg['id'])
|
|
4968
|
-
|
|
4977
|
+
if full
|
|
4978
|
+
if msg.frozen?
|
|
4979
|
+
# Replace frozen hash in filtered_messages with mutable copy
|
|
4980
|
+
idx = @filtered_messages.index { |m| m.equal?(msg) }
|
|
4981
|
+
msg = full.merge('_full_loaded' => true)
|
|
4982
|
+
@filtered_messages[idx] = msg if idx
|
|
4983
|
+
else
|
|
4984
|
+
msg.merge!(full)
|
|
4985
|
+
msg['_full_loaded'] = true
|
|
4986
|
+
end
|
|
4987
|
+
end
|
|
4969
4988
|
end
|
|
4970
4989
|
msg
|
|
4971
4990
|
end
|
|
@@ -4975,7 +4994,7 @@ module Heathrow
|
|
|
4975
4994
|
def reply_to_message(force_editor: false)
|
|
4976
4995
|
msg = current_message
|
|
4977
4996
|
return unless msg
|
|
4978
|
-
ensure_full_message(msg)
|
|
4997
|
+
msg = ensure_full_message(msg)
|
|
4979
4998
|
|
|
4980
4999
|
# Don't allow replying to header messages
|
|
4981
5000
|
if msg['is_header'] || msg['is_channel_header'] || msg['is_thread_header'] || msg['is_dm_header']
|
|
@@ -5004,15 +5023,15 @@ module Heathrow
|
|
|
5004
5023
|
@panes[:bottom].refresh
|
|
5005
5024
|
|
|
5006
5025
|
composed = composer.compose_reply(false)
|
|
5026
|
+
setup_display
|
|
5027
|
+
create_panes
|
|
5028
|
+
render_all
|
|
5007
5029
|
if composed
|
|
5008
5030
|
finalize_compose(source, composed, "Reply cancelled")
|
|
5009
5031
|
else
|
|
5010
5032
|
set_feedback("Reply cancelled", 245, 1)
|
|
5011
5033
|
end
|
|
5012
5034
|
end
|
|
5013
|
-
|
|
5014
|
-
render_message_list
|
|
5015
|
-
render_bottom_bar
|
|
5016
5035
|
end
|
|
5017
5036
|
|
|
5018
5037
|
def chat_reply_context(msg, source_type)
|
|
@@ -5088,7 +5107,7 @@ module Heathrow
|
|
|
5088
5107
|
def reply_all_to_message
|
|
5089
5108
|
msg = current_message
|
|
5090
5109
|
return unless msg
|
|
5091
|
-
ensure_full_message(msg)
|
|
5110
|
+
msg = ensure_full_message(msg)
|
|
5092
5111
|
source_id = msg['source_id']
|
|
5093
5112
|
|
|
5094
5113
|
# Check if source supports replying
|
|
@@ -5106,20 +5125,21 @@ module Heathrow
|
|
|
5106
5125
|
# Compose the reply
|
|
5107
5126
|
composed = composer.compose_reply(true)
|
|
5108
5127
|
|
|
5128
|
+
setup_display
|
|
5129
|
+
create_panes
|
|
5130
|
+
render_all
|
|
5109
5131
|
if composed
|
|
5110
5132
|
finalize_compose(source, composed, "Reply-all cancelled")
|
|
5111
5133
|
else
|
|
5112
5134
|
set_feedback("Reply-all cancelled", 245, 1)
|
|
5113
5135
|
end
|
|
5114
|
-
|
|
5115
|
-
render_bottom_bar
|
|
5116
5136
|
end
|
|
5117
5137
|
|
|
5118
5138
|
def edit_message_content
|
|
5119
5139
|
msg = current_message
|
|
5120
5140
|
return unless msg
|
|
5121
5141
|
return if msg['is_header'] || msg['is_channel_header'] || msg['is_thread_header']
|
|
5122
|
-
ensure_full_message(msg)
|
|
5142
|
+
msg = ensure_full_message(msg)
|
|
5123
5143
|
|
|
5124
5144
|
source_id = msg['source_id']
|
|
5125
5145
|
source = @db.get_source(source_id)
|
|
@@ -5147,7 +5167,7 @@ module Heathrow
|
|
|
5147
5167
|
def forward_message
|
|
5148
5168
|
msg = current_message
|
|
5149
5169
|
return unless msg
|
|
5150
|
-
ensure_full_message(msg)
|
|
5170
|
+
msg = ensure_full_message(msg)
|
|
5151
5171
|
source_id = msg['source_id']
|
|
5152
5172
|
|
|
5153
5173
|
# Check if source supports sending
|
|
@@ -5162,18 +5182,59 @@ module Heathrow
|
|
|
5162
5182
|
@panes[:bottom].text = " Opening editor to forward message...".fg(226)
|
|
5163
5183
|
@panes[:bottom].refresh
|
|
5164
5184
|
|
|
5185
|
+
# Extract attachments BEFORE editor
|
|
5186
|
+
orig_attachments = extract_original_attachments(msg)
|
|
5187
|
+
|
|
5165
5188
|
# Compose the forward
|
|
5166
5189
|
composed = composer.compose_forward
|
|
5167
5190
|
|
|
5191
|
+
# Rebuild panes after editor (vim clears the screen)
|
|
5192
|
+
setup_display
|
|
5193
|
+
create_panes
|
|
5194
|
+
render_all
|
|
5195
|
+
|
|
5168
5196
|
if composed
|
|
5197
|
+
# Include original attachments
|
|
5198
|
+
composed[:attachments] = orig_attachments if orig_attachments && !orig_attachments.empty?
|
|
5169
5199
|
finalize_compose(source, composed, "Forward cancelled")
|
|
5170
5200
|
else
|
|
5171
5201
|
set_feedback("Forward cancelled", 245, 1)
|
|
5172
5202
|
end
|
|
5173
|
-
|
|
5174
|
-
render_bottom_bar
|
|
5175
5203
|
end
|
|
5176
5204
|
|
|
5205
|
+
# Extract original attachments from a message for forwarding
|
|
5206
|
+
def extract_original_attachments(msg)
|
|
5207
|
+
metadata = msg['metadata']
|
|
5208
|
+
metadata = JSON.parse(metadata) if metadata.is_a?(String) rescue nil
|
|
5209
|
+
return nil unless metadata.is_a?(Hash)
|
|
5210
|
+
|
|
5211
|
+
file_path = metadata['maildir_file']
|
|
5212
|
+
return nil unless file_path && File.exist?(file_path)
|
|
5213
|
+
|
|
5214
|
+
require 'mail'
|
|
5215
|
+
require 'tmpdir'
|
|
5216
|
+
mail = Mail.read(file_path)
|
|
5217
|
+
return nil if mail.attachments.empty?
|
|
5218
|
+
|
|
5219
|
+
tmp_dir = File.join(Dir.tmpdir, "heathrow-fwd-#{Process.pid}")
|
|
5220
|
+
FileUtils.mkdir_p(tmp_dir)
|
|
5221
|
+
|
|
5222
|
+
paths = []
|
|
5223
|
+
mail.attachments.each do |att|
|
|
5224
|
+
next unless att.filename
|
|
5225
|
+
out = File.join(tmp_dir, att.filename)
|
|
5226
|
+
File.write(out, att.decoded)
|
|
5227
|
+
paths << out
|
|
5228
|
+
end
|
|
5229
|
+
paths.empty? ? nil : paths
|
|
5230
|
+
rescue => e
|
|
5231
|
+
File.open('/tmp/heathrow-crash.log', 'a') { |f|
|
|
5232
|
+
f.puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} extract_attachments: #{e.class}: #{e.message}"
|
|
5233
|
+
f.puts " #{e.backtrace&.first(3)&.join("\n ")}"
|
|
5234
|
+
}
|
|
5235
|
+
nil
|
|
5236
|
+
end
|
|
5237
|
+
|
|
5177
5238
|
def compose_new_message
|
|
5178
5239
|
# Build list of sendable channels from all DB sources
|
|
5179
5240
|
channels = []
|
|
@@ -5250,6 +5311,9 @@ module Heathrow
|
|
|
5250
5311
|
|
|
5251
5312
|
composed = composer.compose_new
|
|
5252
5313
|
end
|
|
5314
|
+
setup_display
|
|
5315
|
+
create_panes
|
|
5316
|
+
render_all
|
|
5253
5317
|
if composed
|
|
5254
5318
|
finalize_compose(source, composed, "Message cancelled")
|
|
5255
5319
|
else
|
|
@@ -5402,8 +5466,10 @@ module Heathrow
|
|
|
5402
5466
|
setup_display
|
|
5403
5467
|
create_panes
|
|
5404
5468
|
render_all
|
|
5469
|
+
pending_attachments = Array(composed[:attachments]).dup
|
|
5405
5470
|
loop do
|
|
5406
|
-
attachments = prompt_attachments(composed: composed)
|
|
5471
|
+
attachments = prompt_attachments(pending_attachments, composed: composed)
|
|
5472
|
+
pending_attachments = [] # Only seed on first iteration
|
|
5407
5473
|
case attachments
|
|
5408
5474
|
when :postpone
|
|
5409
5475
|
postpone_message(source, composed)
|
|
@@ -7719,7 +7785,7 @@ Required: URL, optional CSS selector
|
|
|
7719
7785
|
|
|
7720
7786
|
msg = current_message
|
|
7721
7787
|
return unless msg
|
|
7722
|
-
ensure_full_message(msg)
|
|
7788
|
+
msg = ensure_full_message(msg)
|
|
7723
7789
|
|
|
7724
7790
|
http_urls = []
|
|
7725
7791
|
|
data/lib/heathrow/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: heathrow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Geir Isene
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-03-
|
|
12
|
+
date: 2026-03-17 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rcurses
|