heathrow 0.7.4 → 0.7.5
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/lib/heathrow/database.rb +13 -0
- data/lib/heathrow/ui/application.rb +271 -40
- data/lib/heathrow/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5276cc34b418c9d3b080eb711a1870857679ca87681c58436e6207ff37e7106
|
|
4
|
+
data.tar.gz: 8a10f16633f2ac610bc191434566f883319b570a2ad773297373a8d5a4e477de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c98df39af32e449f46f7a4d7c58436d88cfd6e32499394624ad74c2b16c4e390512ecbe18fff5fbe0462c1bb76b18921b806a72403aa08e7e39ccfc7ac69ccc
|
|
7
|
+
data.tar.gz: 9fb82e3608cb1fd6d63078b8584aad96deb3e43072421468b4c712d8c3ead53c192b7378c4d1cef75ada0f5f8d430e81d5847d71c27a04accfdf333f59fe01c8
|
data/lib/heathrow/database.rb
CHANGED
|
@@ -358,6 +358,19 @@ module Heathrow
|
|
|
358
358
|
query += " AND source_id = ?"
|
|
359
359
|
params << filters[:source_id]
|
|
360
360
|
end
|
|
361
|
+
|
|
362
|
+
if filters[:source_ids].is_a?(Array) && !filters[:source_ids].empty?
|
|
363
|
+
ph = filters[:source_ids].map { '?' }.join(',')
|
|
364
|
+
query += " AND source_id IN (#{ph})"
|
|
365
|
+
params += filters[:source_ids]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
if filters[:source_name]
|
|
369
|
+
patterns = filters[:source_name].split('|').map(&:strip)
|
|
370
|
+
conditions = patterns.map { "name LIKE ?" }.join(' OR ')
|
|
371
|
+
query += " AND source_id IN (SELECT id FROM sources WHERE #{conditions})"
|
|
372
|
+
params += patterns.map { |p| "%#{p}%" }
|
|
373
|
+
end
|
|
361
374
|
|
|
362
375
|
# Handle sender pattern (supports regex via pipe separation)
|
|
363
376
|
if filters[:sender_pattern]
|
|
@@ -2154,6 +2154,11 @@ module Heathrow
|
|
|
2154
2154
|
# Colorize email content (quote levels + signature)
|
|
2155
2155
|
content = colorize_email_content(content)
|
|
2156
2156
|
|
|
2157
|
+
# Store maildir file path for calendar parser
|
|
2158
|
+
meta = msg['metadata']
|
|
2159
|
+
meta = JSON.parse(meta) if meta.is_a?(String) rescue nil
|
|
2160
|
+
@_current_render_msg_file = meta['maildir_file'] if meta.is_a?(Hash)
|
|
2161
|
+
|
|
2157
2162
|
# Attachment list under header, before body
|
|
2158
2163
|
att_text = format_attachments(msg['attachments'])
|
|
2159
2164
|
|
|
@@ -2175,10 +2180,14 @@ module Heathrow
|
|
|
2175
2180
|
end
|
|
2176
2181
|
html_hint = message_has_html?(msg) ? "HTML mail, press x to open in browser".fg(39) : nil
|
|
2177
2182
|
|
|
2183
|
+
# Parse calendar invites (ICS attachments)
|
|
2184
|
+
cal_text = format_calendar_event(msg['attachments'])
|
|
2185
|
+
|
|
2178
2186
|
full_text = header.join("\n")
|
|
2179
2187
|
full_text += "\n" + att_text if att_text
|
|
2180
2188
|
full_text += "\n" + image_hint if image_hint
|
|
2181
2189
|
full_text += "\n" + html_hint if html_hint
|
|
2190
|
+
full_text += "\n\n" + cal_text if cal_text
|
|
2182
2191
|
full_text += "\n\n" + content
|
|
2183
2192
|
|
|
2184
2193
|
@panes[:right].text = full_text
|
|
@@ -2360,8 +2369,8 @@ module Heathrow
|
|
|
2360
2369
|
if @current_folder
|
|
2361
2370
|
light_cols = "id, source_id, external_id, thread_id, parent_id, sender, sender_name, recipients, subject, substr(content, 1, 200) as content, timestamp, received_at, read AS is_read, starred AS is_starred, archived, labels, metadata, attachments, folder, replied"
|
|
2362
2371
|
results = @db.execute(
|
|
2363
|
-
"SELECT #{light_cols} FROM messages WHERE folder
|
|
2364
|
-
@current_folder, @
|
|
2372
|
+
"SELECT #{light_cols} FROM messages WHERE folder = ? ORDER BY timestamp DESC LIMIT ?",
|
|
2373
|
+
@current_folder, @load_limit
|
|
2365
2374
|
)
|
|
2366
2375
|
@filtered_messages = results
|
|
2367
2376
|
elsif @current_view == 'A'
|
|
@@ -2462,6 +2471,7 @@ module Heathrow
|
|
|
2462
2471
|
when 'subject' then db_filters[:subject_pattern] = value
|
|
2463
2472
|
when 'folder' then db_filters[:maildir_folder] = value
|
|
2464
2473
|
when 'label' then db_filters[:label] = value
|
|
2474
|
+
when 'source' then db_filters[:source_name] = value
|
|
2465
2475
|
end
|
|
2466
2476
|
end
|
|
2467
2477
|
end
|
|
@@ -2514,8 +2524,8 @@ module Heathrow
|
|
|
2514
2524
|
if @current_folder
|
|
2515
2525
|
light_cols = "id, source_id, external_id, thread_id, parent_id, sender, sender_name, recipients, subject, substr(content, 1, 200) as content, timestamp, received_at, read AS is_read, starred AS is_starred, archived, labels, metadata, attachments, folder, replied"
|
|
2516
2526
|
@filtered_messages = @db.execute(
|
|
2517
|
-
"SELECT #{light_cols} FROM messages WHERE folder
|
|
2518
|
-
@current_folder, @
|
|
2527
|
+
"SELECT #{light_cols} FROM messages WHERE folder = ? ORDER BY timestamp DESC LIMIT ?",
|
|
2528
|
+
@current_folder, @load_limit
|
|
2519
2529
|
)
|
|
2520
2530
|
elsif @current_view == 'A'
|
|
2521
2531
|
@filtered_messages = @db.get_messages({}, @load_limit, 0, light: true)
|
|
@@ -2665,6 +2675,7 @@ module Heathrow
|
|
|
2665
2675
|
when 'subject' then db_filters[:subject_pattern] = value
|
|
2666
2676
|
when 'folder' then db_filters[:maildir_folder] = value
|
|
2667
2677
|
when 'label' then db_filters[:label] = value
|
|
2678
|
+
when 'source' then db_filters[:source_name] = value
|
|
2668
2679
|
end
|
|
2669
2680
|
end
|
|
2670
2681
|
end
|
|
@@ -3736,8 +3747,8 @@ module Heathrow
|
|
|
3736
3747
|
def folder_message_count(folder_name)
|
|
3737
3748
|
# Use range query to leverage folder index (5x faster than OR + LIKE)
|
|
3738
3749
|
row = @db.db.get_first_row(
|
|
3739
|
-
"SELECT COUNT(*) as total, SUM(CASE WHEN read = 0 THEN 1 ELSE 0 END) as unread FROM messages WHERE folder
|
|
3740
|
-
[folder_name
|
|
3750
|
+
"SELECT COUNT(*) as total, SUM(CASE WHEN read = 0 THEN 1 ELSE 0 END) as unread FROM messages WHERE folder = ?",
|
|
3751
|
+
[folder_name]
|
|
3741
3752
|
)
|
|
3742
3753
|
{ total: (row && row['total']) || 0, unread: (row && row['unread']) || 0 }
|
|
3743
3754
|
rescue
|
|
@@ -3985,8 +3996,8 @@ module Heathrow
|
|
|
3985
3996
|
@load_limit = 200
|
|
3986
3997
|
light_cols = "id, source_id, external_id, thread_id, parent_id, sender, sender_name, recipients, subject, substr(content, 1, 200) as content, timestamp, received_at, read AS is_read, starred AS is_starred, archived, labels, metadata, attachments, folder, replied"
|
|
3987
3998
|
results = @db.execute(
|
|
3988
|
-
"SELECT #{light_cols} FROM messages WHERE folder
|
|
3989
|
-
folder_name,
|
|
3999
|
+
"SELECT #{light_cols} FROM messages WHERE folder = ? ORDER BY timestamp DESC LIMIT ?",
|
|
4000
|
+
folder_name, @load_limit
|
|
3990
4001
|
)
|
|
3991
4002
|
|
|
3992
4003
|
@filtered_messages = results
|
|
@@ -4864,47 +4875,83 @@ module Heathrow
|
|
|
4864
4875
|
# Notmuch full-text search
|
|
4865
4876
|
def notmuch_search
|
|
4866
4877
|
require_relative '../notmuch'
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4878
|
+
has_notmuch = Heathrow::Notmuch.available?
|
|
4879
|
+
|
|
4880
|
+
# Source picker: let user scope the search
|
|
4881
|
+
sources = @db.get_sources(true) # enabled only
|
|
4882
|
+
scope_hint = sources.each_with_index.map { |s, i| "#{i + 1}:#{s['name']}" }.join(' ')
|
|
4883
|
+
scope = bottom_ask("Search in (Enter=all, #{scope_hint}): ", "")
|
|
4884
|
+
return if scope == 'ESC'
|
|
4885
|
+
|
|
4886
|
+
selected_source_ids = nil
|
|
4887
|
+
scope_label = "all"
|
|
4888
|
+
if scope && !scope.strip.empty?
|
|
4889
|
+
# Parse source selection (comma-separated numbers or name fragment)
|
|
4890
|
+
selected = []
|
|
4891
|
+
scope.split(',').each do |part|
|
|
4892
|
+
part = part.strip
|
|
4893
|
+
if part =~ /^\d+$/
|
|
4894
|
+
idx = part.to_i - 1
|
|
4895
|
+
selected << sources[idx] if idx >= 0 && idx < sources.size
|
|
4896
|
+
else
|
|
4897
|
+
# Name fragment match
|
|
4898
|
+
sources.each { |s| selected << s if s['name'].downcase.include?(part.downcase) }
|
|
4899
|
+
end
|
|
4900
|
+
end
|
|
4901
|
+
if selected.any?
|
|
4902
|
+
selected_source_ids = selected.map { |s| s['id'] }
|
|
4903
|
+
scope_label = selected.map { |s| s['name'] }.join(', ')
|
|
4904
|
+
end
|
|
4870
4905
|
end
|
|
4871
4906
|
|
|
4872
|
-
query = bottom_ask("Search
|
|
4907
|
+
query = bottom_ask("Search#{scope_label != 'all' ? " [#{scope_label}]" : ''}: ", "")
|
|
4873
4908
|
return if query.nil? || query.strip.empty?
|
|
4874
4909
|
|
|
4875
4910
|
@panes[:bottom].text = " Searching...".fg(226)
|
|
4876
4911
|
@panes[:bottom].refresh
|
|
4877
4912
|
|
|
4878
|
-
files = Heathrow::Notmuch.search_files(query)
|
|
4879
|
-
if files.empty?
|
|
4880
|
-
set_feedback("No results for: #{query}", 226, 3)
|
|
4881
|
-
return
|
|
4882
|
-
end
|
|
4883
|
-
|
|
4884
|
-
# Map file paths back to Heathrow messages via metadata.maildir_file
|
|
4885
|
-
# Use basename matching since paths may differ slightly
|
|
4886
|
-
basenames = files.map { |f| File.basename(f) }
|
|
4887
|
-
placeholders = basenames.map { '?' }.join(',')
|
|
4888
|
-
|
|
4889
|
-
# Search by external_id (which is the filename basename)
|
|
4890
4913
|
results = []
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4914
|
+
|
|
4915
|
+
# Use notmuch for Maildir sources (fast indexed search)
|
|
4916
|
+
if has_notmuch && (selected_source_ids.nil? || sources.any? { |s| selected_source_ids&.include?(s['id']) && s['plugin_type'] == 'maildir' })
|
|
4917
|
+
files = Heathrow::Notmuch.search_files(query)
|
|
4918
|
+
unless files.empty?
|
|
4919
|
+
basenames = files.map { |f| File.basename(f) }
|
|
4920
|
+
basenames.each_slice(100) do |batch|
|
|
4921
|
+
ph = batch.map { '?' }.join(',')
|
|
4922
|
+
sql = "SELECT * FROM messages WHERE external_id IN (#{ph})"
|
|
4923
|
+
params = batch.dup
|
|
4924
|
+
if selected_source_ids
|
|
4925
|
+
sid_ph = selected_source_ids.map { '?' }.join(',')
|
|
4926
|
+
sql += " AND source_id IN (#{sid_ph})"
|
|
4927
|
+
params += selected_source_ids
|
|
4928
|
+
end
|
|
4929
|
+
rows = @db.execute(sql, *params)
|
|
4930
|
+
rows.each do |row|
|
|
4931
|
+
row['recipients'] = JSON.parse(row['recipients']) if row['recipients'].is_a?(String)
|
|
4932
|
+
row['metadata'] = JSON.parse(row['metadata']) if row['metadata'].is_a?(String)
|
|
4933
|
+
row['labels'] = JSON.parse(row['labels']) if row['labels'].is_a?(String)
|
|
4934
|
+
row['attachments'] = JSON.parse(row['attachments']) if row['attachments'].is_a?(String)
|
|
4935
|
+
end
|
|
4936
|
+
results.concat(rows)
|
|
4937
|
+
end
|
|
4902
4938
|
end
|
|
4903
|
-
|
|
4939
|
+
end
|
|
4940
|
+
|
|
4941
|
+
# DB search for non-Maildir sources (or if notmuch unavailable)
|
|
4942
|
+
non_maildir_ids = if selected_source_ids
|
|
4943
|
+
selected_source_ids.select { |sid| sources.find { |s| s['id'] == sid && s['plugin_type'] != 'maildir' } }
|
|
4944
|
+
else
|
|
4945
|
+
sources.select { |s| s['plugin_type'] != 'maildir' }.map { |s| s['id'] }
|
|
4946
|
+
end
|
|
4947
|
+
if non_maildir_ids.any?
|
|
4948
|
+
db_filters = { search: query, source_ids: non_maildir_ids }
|
|
4949
|
+
db_results = @db.get_messages(db_filters, 500, 0, light: false)
|
|
4950
|
+
results.concat(db_results)
|
|
4904
4951
|
end
|
|
4905
4952
|
|
|
4906
4953
|
if results.empty?
|
|
4907
|
-
set_feedback("#{
|
|
4954
|
+
set_feedback("No results for: #{query}", 226, 3)
|
|
4908
4955
|
return
|
|
4909
4956
|
end
|
|
4910
4957
|
|
|
@@ -4912,11 +4959,12 @@ module Heathrow
|
|
|
4912
4959
|
@current_view = 'A'
|
|
4913
4960
|
@in_source_view = false
|
|
4914
4961
|
@panes[:right].content_update = true
|
|
4915
|
-
@current_source_filter = "Search: #{query}"
|
|
4962
|
+
@current_source_filter = "Search: #{query}#{scope_label != 'all' ? " [#{scope_label}]" : ''}"
|
|
4916
4963
|
@filtered_messages = results
|
|
4917
4964
|
sort_messages
|
|
4918
4965
|
@index = 0
|
|
4919
|
-
|
|
4966
|
+
reset_threading if respond_to?(:reset_threading)
|
|
4967
|
+
set_feedback("#{results.size} results for: #{query}", 156, 0)
|
|
4920
4968
|
render_all
|
|
4921
4969
|
end
|
|
4922
4970
|
|
|
@@ -7383,7 +7431,16 @@ Required: URL, optional CSS selector
|
|
|
7383
7431
|
new_rules << { 'field' => 'subject', 'op' => 'like', 'value' => subject_input }
|
|
7384
7432
|
end
|
|
7385
7433
|
|
|
7386
|
-
# 6.
|
|
7434
|
+
# 6. Source filter
|
|
7435
|
+
current_source = current_vals['source_like'] || ''
|
|
7436
|
+
source_names = @db.get_sources(true).map { |s| s['name'] }.join(', ')
|
|
7437
|
+
source_input = bottom_ask("Source (#{source_names} - ESC cancel): ", current_source)
|
|
7438
|
+
return render_all if source_input.nil?
|
|
7439
|
+
unless source_input.empty?
|
|
7440
|
+
new_rules << { 'field' => 'source', 'op' => 'like', 'value' => source_input }
|
|
7441
|
+
end
|
|
7442
|
+
|
|
7443
|
+
# 7. Read status
|
|
7387
7444
|
current_read = current_vals['read_=']
|
|
7388
7445
|
default_read = current_read == false ? 'y' : (current_read == true ? 'n' : '')
|
|
7389
7446
|
read_input = bottom_ask("Unread only? (y/n/Enter for all, ESC cancel): ", default_read)
|
|
@@ -8008,6 +8065,180 @@ Required: URL, optional CSS selector
|
|
|
8008
8065
|
end
|
|
8009
8066
|
|
|
8010
8067
|
# Format attachment list for display
|
|
8068
|
+
# Parse and format calendar events from ICS attachments
|
|
8069
|
+
def format_calendar_event(attachments)
|
|
8070
|
+
attachments = [] unless attachments.is_a?(Array)
|
|
8071
|
+
# Find ICS data from attachments or inline MIME parts
|
|
8072
|
+
ics_data = nil
|
|
8073
|
+
begin
|
|
8074
|
+
require 'mail'
|
|
8075
|
+
|
|
8076
|
+
# First check attachments array
|
|
8077
|
+
ics_att = attachments.is_a?(Array) && attachments.find do |att|
|
|
8078
|
+
ct = (att['content_type'] || '').downcase
|
|
8079
|
+
name = (att['name'] || att['filename'] || '').downcase
|
|
8080
|
+
ct.include?('calendar') || ct.include?('ics') || name.end_with?('.ics')
|
|
8081
|
+
end
|
|
8082
|
+
|
|
8083
|
+
# Get the maildir file path (from attachment or message metadata)
|
|
8084
|
+
file = ics_att['source_file'] if ics_att
|
|
8085
|
+
file ||= @_current_render_msg_file # Set by render_message_content
|
|
8086
|
+
return nil unless file && File.exist?(file)
|
|
8087
|
+
|
|
8088
|
+
# Parse MIME parts for calendar data
|
|
8089
|
+
mail = Mail.read(file)
|
|
8090
|
+
if mail.multipart?
|
|
8091
|
+
mail.parts.each do |part|
|
|
8092
|
+
ct = (part.content_type || '').downcase
|
|
8093
|
+
if ct.include?('calendar') || ct.include?('ics')
|
|
8094
|
+
ics_data = part.decoded
|
|
8095
|
+
break
|
|
8096
|
+
end
|
|
8097
|
+
if part.multipart?
|
|
8098
|
+
part.parts.each do |sub|
|
|
8099
|
+
sct = (sub.content_type || '').downcase
|
|
8100
|
+
if sct.include?('calendar') || sct.include?('ics')
|
|
8101
|
+
ics_data = sub.decoded
|
|
8102
|
+
break
|
|
8103
|
+
end
|
|
8104
|
+
end
|
|
8105
|
+
break if ics_data
|
|
8106
|
+
end
|
|
8107
|
+
end
|
|
8108
|
+
end
|
|
8109
|
+
ics_data ||= File.read(file) if file.end_with?('.ics')
|
|
8110
|
+
return nil unless ics_data && ics_data.include?('BEGIN:')
|
|
8111
|
+
|
|
8112
|
+
# Use VcalView parser
|
|
8113
|
+
# Use basic inline ICS parser
|
|
8114
|
+
event = parse_ics_basic(ics_data)
|
|
8115
|
+
return nil unless event
|
|
8116
|
+
|
|
8117
|
+
# Format the event for display
|
|
8118
|
+
lines = []
|
|
8119
|
+
lines << ("─" * 50).fg(238)
|
|
8120
|
+
lines << "Calendar Event".b.fg(226)
|
|
8121
|
+
lines << ""
|
|
8122
|
+
lines << "WHAT: #{event[:summary]}".fg(156) if event[:summary]
|
|
8123
|
+
if event[:dates]
|
|
8124
|
+
when_str = event[:dates]
|
|
8125
|
+
when_str += " (#{event[:weekday]})" if event[:weekday]
|
|
8126
|
+
when_str += ", #{event[:times]}" if event[:times]
|
|
8127
|
+
lines << "WHEN: #{when_str}".fg(39)
|
|
8128
|
+
end
|
|
8129
|
+
lines << "WHERE: #{event[:location]}".fg(45) if event[:location] && !event[:location].to_s.empty?
|
|
8130
|
+
lines << "RECUR: #{event[:recurrence]}".fg(180) if event[:recurrence]
|
|
8131
|
+
lines << "STATUS: #{event[:status]}".fg(245) if event[:status]
|
|
8132
|
+
lines << ""
|
|
8133
|
+
lines << "ORGANIZER: #{event[:organizer]}".fg(2) if event[:organizer]
|
|
8134
|
+
if event[:participants] && !event[:participants].to_s.strip.empty?
|
|
8135
|
+
lines << "PARTICIPANTS:".fg(2)
|
|
8136
|
+
lines << event[:participants].fg(245)
|
|
8137
|
+
end
|
|
8138
|
+
# Skip description (email body already shows it, and ICS descriptions
|
|
8139
|
+
# often contain raw URLs that can overflow the pane)
|
|
8140
|
+
lines << ("─" * 50).fg(238)
|
|
8141
|
+
lines.join("\n")
|
|
8142
|
+
rescue => e
|
|
8143
|
+
nil # Don't crash on calendar parse errors
|
|
8144
|
+
end
|
|
8145
|
+
end
|
|
8146
|
+
|
|
8147
|
+
# Basic ICS parsing fallback (when VcalView is not available)
|
|
8148
|
+
def parse_ics_basic(ics)
|
|
8149
|
+
# Extract only the VEVENT section (ignore VTIMEZONE which has dummy dates)
|
|
8150
|
+
vevent = ics[/BEGIN:VEVENT(.*?)END:VEVENT/m, 1]
|
|
8151
|
+
return nil unless vevent
|
|
8152
|
+
|
|
8153
|
+
# Unfold continuation lines (RFC 5545: lines starting with space are continuations)
|
|
8154
|
+
vevent = vevent.gsub(/\r?\n[ \t]/, '')
|
|
8155
|
+
|
|
8156
|
+
event = {}
|
|
8157
|
+
|
|
8158
|
+
# SUMMARY (strip LANGUAGE= and other params before the colon-value)
|
|
8159
|
+
if vevent =~ /^SUMMARY[^:]*:(.*)$/i
|
|
8160
|
+
event[:summary] = $1.strip
|
|
8161
|
+
end
|
|
8162
|
+
|
|
8163
|
+
# DTSTART with TZID
|
|
8164
|
+
if vevent =~ /^DTSTART;TZID=[^:]*:(\d{8})T?(\d{4,6})?/i
|
|
8165
|
+
d = $1; t = $2
|
|
8166
|
+
event[:dates] = "#{d[0,4]}-#{d[4,2]}-#{d[6,2]}"
|
|
8167
|
+
event[:times] = t ? "#{t[0,2]}:#{t[2,2]}" : "All day"
|
|
8168
|
+
begin
|
|
8169
|
+
dobj = Time.parse(event[:dates])
|
|
8170
|
+
event[:weekday] = dobj.strftime('%A')
|
|
8171
|
+
rescue; end
|
|
8172
|
+
elsif vevent =~ /^DTSTART;VALUE=DATE:(\d{8})/i
|
|
8173
|
+
d = $1
|
|
8174
|
+
event[:dates] = "#{d[0,4]}-#{d[4,2]}-#{d[6,2]}"
|
|
8175
|
+
event[:times] = "All day"
|
|
8176
|
+
elsif vevent =~ /^DTSTART:(\d{8})T?(\d{4,6})?(Z)?/i
|
|
8177
|
+
d = $1; t = $2; utc = $3
|
|
8178
|
+
event[:dates] = "#{d[0,4]}-#{d[4,2]}-#{d[6,2]}"
|
|
8179
|
+
if t
|
|
8180
|
+
# Convert UTC times to local
|
|
8181
|
+
if utc
|
|
8182
|
+
utc_time = Time.utc(d[0,4].to_i, d[4,2].to_i, d[6,2].to_i, t[0,2].to_i, t[2,2].to_i)
|
|
8183
|
+
local = utc_time.localtime
|
|
8184
|
+
event[:dates] = local.strftime('%Y-%m-%d')
|
|
8185
|
+
event[:times] = local.strftime('%H:%M')
|
|
8186
|
+
event[:weekday] = local.strftime('%A')
|
|
8187
|
+
else
|
|
8188
|
+
event[:times] = "#{t[0,2]}:#{t[2,2]}"
|
|
8189
|
+
begin
|
|
8190
|
+
event[:weekday] = Time.parse(event[:dates]).strftime('%A')
|
|
8191
|
+
rescue; end
|
|
8192
|
+
end
|
|
8193
|
+
else
|
|
8194
|
+
event[:times] = "All day"
|
|
8195
|
+
end
|
|
8196
|
+
end
|
|
8197
|
+
|
|
8198
|
+
# DTEND
|
|
8199
|
+
if vevent =~ /^DTEND;TZID=[^:]*:(\d{8})T?(\d{4,6})?/i
|
|
8200
|
+
d = $1; t = $2
|
|
8201
|
+
end_date = "#{d[0,4]}-#{d[4,2]}-#{d[6,2]}"
|
|
8202
|
+
end_time = t ? "#{t[0,2]}:#{t[2,2]}" : nil
|
|
8203
|
+
elsif vevent =~ /^DTEND:(\d{8})T?(\d{4,6})?(Z)?/i
|
|
8204
|
+
d = $1; t = $2; utc = $3
|
|
8205
|
+
if t && utc
|
|
8206
|
+
utc_time = Time.utc(d[0,4].to_i, d[4,2].to_i, d[6,2].to_i, t[0,2].to_i, t[2,2].to_i)
|
|
8207
|
+
local = utc_time.localtime
|
|
8208
|
+
end_date = local.strftime('%Y-%m-%d')
|
|
8209
|
+
end_time = local.strftime('%H:%M')
|
|
8210
|
+
else
|
|
8211
|
+
end_date = "#{d[0,4]}-#{d[4,2]}-#{d[6,2]}"
|
|
8212
|
+
end_time = t ? "#{t[0,2]}:#{t[2,2]}" : nil
|
|
8213
|
+
end
|
|
8214
|
+
event[:dates] += " - #{end_date}" if end_date && end_date != event[:dates]
|
|
8215
|
+
event[:times] += " - #{end_time}" if end_time && end_time != event[:times]
|
|
8216
|
+
end
|
|
8217
|
+
|
|
8218
|
+
# LOCATION (strip params)
|
|
8219
|
+
if vevent =~ /^LOCATION[^:]*:(.*)$/i
|
|
8220
|
+
event[:location] = $1.strip
|
|
8221
|
+
end
|
|
8222
|
+
|
|
8223
|
+
# ORGANIZER
|
|
8224
|
+
if vevent =~ /^ORGANIZER.*CN=([^;:]+)/i
|
|
8225
|
+
event[:organizer] = $1.strip
|
|
8226
|
+
elsif vevent =~ /^ORGANIZER.*MAILTO:(.+)$/i
|
|
8227
|
+
event[:organizer] = $1.strip
|
|
8228
|
+
end
|
|
8229
|
+
|
|
8230
|
+
# ATTENDEES
|
|
8231
|
+
attendees = vevent.scan(/^ATTENDEE.*CN=([^;:]+)/i).flatten
|
|
8232
|
+
if attendees.any?
|
|
8233
|
+
event[:participants] = attendees.map { |a| " #{a.strip}" }.join("\n")
|
|
8234
|
+
end
|
|
8235
|
+
|
|
8236
|
+
# STATUS
|
|
8237
|
+
event[:status] = $1.strip.capitalize if vevent =~ /^STATUS:(.*)$/i
|
|
8238
|
+
|
|
8239
|
+
event.empty? ? nil : event
|
|
8240
|
+
end
|
|
8241
|
+
|
|
8011
8242
|
def format_attachments(attachments)
|
|
8012
8243
|
return nil unless attachments.is_a?(Array) && !attachments.empty?
|
|
8013
8244
|
lines = []
|
data/lib/heathrow/version.rb
CHANGED