mournmail 0.3.2 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55cea609c299af267b279b8434b661ec471776fe26c5175fdc363a527cc3cc7e
4
- data.tar.gz: b77b32b8fb3923a8e545cd1cd5084dab346974a4f9aa210ae353f76e17839370
3
+ metadata.gz: 8cbf82ccf54ab48f7596d24aaacb4673ca8f8271f304264df7aa96a932dac2fb
4
+ data.tar.gz: facbe695e21c68a1b5c50a9bb9d50894f948e09bab2a436b2174656953b809bf
5
5
  SHA512:
6
- metadata.gz: a891f54dc0e39e09d72a5a8eb9e0272560226fe417eb809462f245bf77e535a9dcaacdf34600e45933723dd0b81378d64e73f1538dcb8c5f2e7c4ffea370078a
7
- data.tar.gz: f0bee693b1b02a51449e38ca5d001a085f999040be22dcb5d29e1782dffd3dd31698312b9abde9f1fbc57528a72e031df05fa75ed4a6ee2c7535cd318063991e
6
+ metadata.gz: 49c6ba544f435ea5eb150b510e26d18ab0bcd0fddd1a1a147dc0aee84bde05656bb153f44628ee0828b2e02c8f6ccc27b75c7e61b34ca0511c0eb2f074240e1e
7
+ data.tar.gz: 43accf4207ec48914c79940d2c45ce931beab826759b3e64174257891bf70d741913800aa976b41c19a2b47d1a0c8e32a00215995889058fdee5c82b4bc6e5e0
data/README.md CHANGED
@@ -33,7 +33,11 @@ CONFIG[:mournmail_accounts] = {
33
33
  },
34
34
  spam_mailbox: "spam",
35
35
  outbox_mailbox: "outbox",
36
- archive_mailbox_format: "archive/%Y"
36
+ archive_mailbox_format: "archive/%Y",
37
+ signature: <<~EOF
38
+ --
39
+ Shugo Maeda <shugo@example.com>
40
+ EOF
37
41
  },
38
42
  "gmail.com" => {
39
43
  from: "Example <example@gmail.com>",
@@ -54,7 +58,7 @@ CONFIG[:mournmail_accounts] = {
54
58
  password: File.read(File.expand_path("~/.textbringer/gmail_passwd")).chomp,
55
59
  ssl: true
56
60
  },
57
- spam_mailbox: "迷惑メール",
61
+ spam_mailbox: "[Gmail]/迷惑メール",
58
62
  archive_mailbox_format: false
59
63
  },
60
64
  }
@@ -11,6 +11,14 @@ define_command(:mournmail_visit_mailbox, doc: "Visit mailbox") do
11
11
  end
12
12
  end
13
13
 
14
+ define_command(:mournmail_visit_spam_mailbox, doc: "Visit spam mailbox") do
15
+ mailbox = Mournmail.account_config[:spam_mailbox]
16
+ if mailbox.nil?
17
+ raise EditorError, "spam_mailbox is not specified"
18
+ end
19
+ mournmail_visit_mailbox(Net::IMAP.encode_utf7(mailbox))
20
+ end
21
+
14
22
  define_command(:mournmail_summary_sync, doc: "Sync summary.") do
15
23
  |mailbox = (Mournmail.current_mailbox || "INBOX"),
16
24
  all = current_prefix_arg|
@@ -61,9 +69,10 @@ define_command(:mail, doc: "Write a new mail.") do
61
69
  User-Agent: Mournmail/#{Mournmail::VERSION} Textbringer/#{Textbringer::VERSION} Ruby/#{RUBY_VERSION}
62
70
  --text follows this line--
63
71
  EOF
64
- re_search_backward(/^To:/)
65
- end_of_line
72
+ beginning_of_buffer
73
+ re_search_forward(/^To: */)
66
74
  if run_hooks
75
+ Mournmail.insert_signature
67
76
  run_hooks(:mournmail_draft_setup_hook)
68
77
  end
69
78
  end
@@ -14,6 +14,13 @@ module Textbringer
14
14
  "X-Mailer",
15
15
  "Content-Type"
16
16
  ]
17
+ CONFIG[:mournmail_quote_header_fields] = [
18
+ "Subject",
19
+ "Date",
20
+ "From",
21
+ "To",
22
+ "Cc"
23
+ ]
17
24
  CONFIG[:mournmail_imap_connect_timeout] = 10
18
25
  CONFIG[:mournmail_keep_alive_interval] = 60
19
26
  case RUBY_PLATFORM
@@ -28,4 +35,5 @@ module Textbringer
28
35
  CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
29
36
  end
30
37
  CONFIG[:mournmail_addresses_path] = File.expand_path("~/.addresses")
38
+ CONFIG[:mournmail_signature_regexp] = /^\n-- /
31
39
  end
@@ -8,6 +8,7 @@ module Mournmail
8
8
  MAIL_MODE_MAP.define_key("\C-c\C-xv", :draft_pgp_sign_command)
9
9
  MAIL_MODE_MAP.define_key("\C-c\C-xe", :draft_pgp_encrypt_command)
10
10
  MAIL_MODE_MAP.define_key("\C-c\t", :insert_signature_command)
11
+ MAIL_MODE_MAP.define_key("\C-c@", :draft_change_account_command)
11
12
 
12
13
  define_syntax :field_name, /^[A-Za-z\-]+: /
13
14
  define_syntax :quotation, /^>.*/
@@ -68,12 +69,15 @@ module Mournmail
68
69
  content: File.read(file),
69
70
  encoding: "binary")
70
71
  end
71
- conf = Mournmail.account_config
72
+ account = @buffer[:mournmail_delivery_account] ||
73
+ Mournmail.current_account
74
+ conf = CONFIG[:mournmail_accounts][account]
72
75
  options = @buffer[:mournmail_delivery_options] ||
73
76
  conf[:delivery_options]
74
77
  if options[:authentication] == "gmail"
78
+ token = Mournmail.google_access_token(account)
75
79
  options = options.merge(authentication: "xoauth2",
76
- password: Mournmail.google_access_token)
80
+ password: token)
77
81
  end
78
82
  m.delivery_method(@buffer[:mournmail_delivery_method] ||
79
83
  conf[:delivery_method],
@@ -186,6 +190,23 @@ module Mournmail
186
190
  @buffer.insert(CONFIG[:signature])
187
191
  end
188
192
 
193
+ define_local_command(:draft_change_account, doc: "Change account.") do
194
+ |account = Mournmail.read_account_name("Change account: ")|
195
+ from = CONFIG[:mournmail_accounts][account][:from]
196
+ @buffer[:mournmail_delivery_account] = account
197
+ @buffer.save_excursion do
198
+ @buffer.beginning_of_buffer
199
+ @buffer.re_search_forward(/^From:.*/)
200
+ @buffer.replace_match("From: " + from)
201
+ @buffer.end_of_buffer
202
+ if @buffer.re_search_backward(CONFIG[:mournmail_signature_regexp],
203
+ raise_error: false)
204
+ @buffer.delete_region(@buffer.point, @buffer.point_max)
205
+ end
206
+ Mournmail.insert_signature
207
+ end
208
+ end
209
+
189
210
  private
190
211
 
191
212
  def end_of_header
@@ -12,7 +12,7 @@ module Mournmail
12
12
 
13
13
  # See http://nihongo.jp/support/mail_guide/dev_guide.txt
14
14
  MAILTO_REGEXP = URI.regexp("mailto")
15
- URI_REGEXP = /(https?|ftp):\/\/[^  \t\n>)"]*[^]  \t\n>.,:)"]+|#{MAILTO_REGEXP}/
15
+ URI_REGEXP = /(https?|ftp):\/\/[^  \t\n>)"]*[^\]  \t\n>.,:)"]+|#{MAILTO_REGEXP}/
16
16
  MIME_REGEXP = /^\[(([0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*|PGP\/MIME .*)\]$/
17
17
  URI_OR_MIME_REGEXP = /#{URI_REGEXP}|#{MIME_REGEXP}/
18
18
 
@@ -24,6 +24,7 @@ module Mournmail
24
24
  def initialize(buffer)
25
25
  super(buffer)
26
26
  buffer.keymap = MESSAGE_MODE_MAP
27
+ @attached_file = nil
27
28
  end
28
29
 
29
30
  define_local_command(:message_open_link_or_part,
@@ -52,6 +53,9 @@ module Mournmail
52
53
 
53
54
  define_local_command(:message_next_link_or_part,
54
55
  doc: "Go to the next link or MIME part.") do
56
+ if @buffer.looking_at?(URI_OR_MIME_REGEXP)
57
+ @buffer.forward_char
58
+ end
55
59
  if @buffer.re_search_forward(URI_OR_MIME_REGEXP, raise_error: false)
56
60
  goto_char(@buffer.match_beginning(0))
57
61
  else
@@ -76,8 +80,8 @@ module Mournmail
76
80
 
77
81
  def part_file_name(part)
78
82
  file_name =
79
- part["content-disposition"]&.parameters&.[]("filename") ||
80
- part["content-type"]&.parameters&.[]("name") ||
83
+ (part["content-disposition"]&.parameters&.[]("filename") rescue nil) ||
84
+ (part["content-type"]&.parameters&.[]("name") rescue nil) ||
81
85
  part_default_file_name(part)
82
86
  decoded_file_name = Mail::Encodings.decode_encode(file_name, :decode)
83
87
  if /\A([A-Za-z0-9_\-]+)'(?:[A-Za-z0-9_\-])*'(.*)/ =~ decoded_file_name
@@ -130,18 +134,20 @@ module Mournmail
130
134
  else
131
135
  file_name = "mournmail"
132
136
  end
133
- f = Tempfile.open(file_name, binmode: true)
137
+ @attached_file = Tempfile.open(file_name, binmode: true)
134
138
  s = part.decoded
135
- if part.charset
139
+ if part.content_type == "text/html"
140
+ s = s.sub(/<meta http-equiv="content-type".*?>/i, "")
141
+ elsif part.charset
136
142
  s = s.encode(part.charset)
137
143
  end
138
- f.write(s)
139
- f.close
140
- if ext == "txt"
141
- find_file(f.path)
144
+ @attached_file.write(s)
145
+ @attached_file.close
146
+ if part.main_type == "text" && part.sub_type != "html"
147
+ find_file(@attached_file.path)
142
148
  else
143
149
  background do
144
- system(*CONFIG[:mournmail_file_open_comamnd], f.path,
150
+ system(*CONFIG[:mournmail_file_open_comamnd], @attached_file.path,
145
151
  out: File::NULL, err: File::NULL)
146
152
  end
147
153
  end
@@ -8,8 +8,8 @@ module Mournmail
8
8
  render_header + "\n" + render_body(indices)
9
9
  end
10
10
 
11
- def render_header
12
- CONFIG[:mournmail_display_header_fields].map { |name|
11
+ def render_header(fields = CONFIG[:mournmail_display_header_fields])
12
+ fields.map { |name|
13
13
  val = self[name]&.to_s&.gsub(/\t/, " ")
14
14
  val ? "#{name}: #{val}\n" : ""
15
15
  }.join
@@ -41,11 +41,36 @@ module Mournmail
41
41
  else
42
42
  type = Mail::Encodings.decode_encode(self["content-type"].to_s,
43
43
  :decode) rescue
44
- "broken/type; error=\"#{$!} (#{$!.class})\""
44
+ "broken/type; error=\"#{$!} (#{$!.class})\""
45
45
  "[0 #{type}]\n"
46
46
  end + pgp_signature
47
47
  end
48
48
 
49
+ def render_text(indices = [])
50
+ if HAVE_MAIL_GPG && encrypted?
51
+ mail = decrypt(verify: true)
52
+ return mail.render_text(indices)
53
+ end
54
+ if multipart?
55
+ parts.each_with_index.map { |part, i|
56
+ if sub_type == "alternative" && i > 0
57
+ ""
58
+ else
59
+ part.render_text([*indices, i])
60
+ end
61
+ }.join("\n")
62
+ elsif main_type.nil? || main_type == "text"
63
+ s = Mournmail.to_utf8(body.decoded, charset)
64
+ if sub_type == "html"
65
+ Html2Text.convert(s)
66
+ else
67
+ s
68
+ end
69
+ else
70
+ ""
71
+ end
72
+ end
73
+
49
74
  def dig_part(i, *rest_indices)
50
75
  if HAVE_MAIL_GPG && encrypted?
51
76
  mail = decrypt(verify: true)
@@ -91,7 +116,39 @@ module Mournmail
91
116
  :decode) rescue
92
117
  "broken/type; error=\"#{$!} (#{$!.class})\""
93
118
  "[#{index} #{type}]\n" +
94
- (no_content ? "" : render_content(indices))
119
+ render_content(indices, no_content)
120
+ end
121
+
122
+ def render_text(indices)
123
+ if multipart?
124
+ parts.each_with_index.map { |part, i|
125
+ if sub_type == "alternative" && i > 0
126
+ ""
127
+ else
128
+ part.render_text([*indices, i])
129
+ end
130
+ }.join("\n")
131
+ else
132
+ if main_type == "message" && sub_type == "rfc822"
133
+ mail = Mail.new(body.raw_source)
134
+ mail.render_header(CONFIG[:mournmail_quote_header_fields]) +
135
+ "\n" + mail.render_text(indices)
136
+ elsif attachment?
137
+ ""
138
+ else
139
+ if main_type == "text"
140
+ if sub_type == "html"
141
+ Html2Text.convert(decoded).sub(/(?<!\n)\z/, "\n")
142
+ else
143
+ decoded.sub(/(?<!\n)\z/, "\n").gsub(/\r\n/, "\n")
144
+ end
145
+ else
146
+ ""
147
+ end
148
+ end
149
+ end
150
+ rescue => e
151
+ ""
95
152
  end
96
153
 
97
154
  def dig_part(i, *rest_indices)
@@ -110,26 +167,29 @@ module Mournmail
110
167
 
111
168
  private
112
169
 
113
- def render_content(indices)
170
+ def render_content(indices, no_content)
114
171
  if multipart?
115
172
  parts.each_with_index.map { |part, i|
116
- no_content = sub_type == "alternative" && i > 0
117
- part.render([*indices, i], no_content)
173
+ part.render([*indices, i],
174
+ no_content || sub_type == "alternative" && i > 0)
118
175
  }.join
119
- elsif main_type == "message" && sub_type == "rfc822"
120
- mail = Mail.new(body.raw_source)
121
- mail.render(indices)
122
- elsif attachment?
123
- ""
124
176
  else
125
- if main_type == "text"
126
- if sub_type == "html"
127
- Html2Text.convert(decoded).sub(/(?<!\n)\z/, "\n")
177
+ return "" if no_content
178
+ if main_type == "message" && sub_type == "rfc822"
179
+ mail = Mail.new(body.raw_source)
180
+ mail.render(indices)
181
+ elsif attachment?
182
+ ""
183
+ else
184
+ if main_type == "text"
185
+ if sub_type == "html"
186
+ Html2Text.convert(decoded).sub(/(?<!\n)\z/, "\n")
187
+ else
188
+ decoded.sub(/(?<!\n)\z/, "\n").gsub(/\r\n/, "\n")
189
+ end
128
190
  else
129
- decoded.sub(/(?<!\n)\z/, "\n").gsub(/\r\n/, "\n")
191
+ ""
130
192
  end
131
- else
132
- ""
133
193
  end
134
194
  end
135
195
  rescue => e
@@ -141,7 +141,8 @@ module Mournmail
141
141
  end
142
142
  s = data[0].attr["BODY[]"]
143
143
  mail = Mournmail.parse_mail(s)
144
- if @mailbox != Mournmail.account_config[:spam_mailbox]
144
+ spam_mailbox = Mournmail.account_config[:spam_mailbox]
145
+ if @mailbox != Net::IMAP.encode_utf7(spam_mailbox)
145
146
  item.cache_id = Mournmail.write_mail_cache(s)
146
147
  Mournmail.index_mail(item.cache_id, mail)
147
148
  end
@@ -25,6 +25,7 @@ module Mournmail
25
25
  SUMMARY_MODE_MAP.define_key("*t", :summary_mark_unflagged_command)
26
26
  SUMMARY_MODE_MAP.define_key("y", :summary_archive_command)
27
27
  SUMMARY_MODE_MAP.define_key("o", :summary_refile_command)
28
+ SUMMARY_MODE_MAP.define_key("!", :summary_refile_spam_command)
28
29
  SUMMARY_MODE_MAP.define_key("p", :summary_prefetch_command)
29
30
  SUMMARY_MODE_MAP.define_key("X", :summary_expunge_command)
30
31
  SUMMARY_MODE_MAP.define_key("v", :summary_view_source_command)
@@ -33,6 +34,7 @@ module Mournmail
33
34
  SUMMARY_MODE_MAP.define_key("k", :previous_line)
34
35
  SUMMARY_MODE_MAP.define_key("j", :next_line)
35
36
  SUMMARY_MODE_MAP.define_key("m", :mournmail_visit_mailbox)
37
+ SUMMARY_MODE_MAP.define_key("S", :mournmail_visit_spam_mailbox)
36
38
  SUMMARY_MODE_MAP.define_key("/", :summary_search_command)
37
39
  SUMMARY_MODE_MAP.define_key("t", :summary_show_thread_command)
38
40
  SUMMARY_MODE_MAP.define_key("@", :summary_change_account_command)
@@ -98,7 +100,7 @@ module Mournmail
98
100
  |reply_all = current_prefix_arg|
99
101
  Mournmail.background do
100
102
  mail = read_current_mail[0]
101
- body = mail.render_body
103
+ body = mail.render_text
102
104
  foreground do
103
105
  Window.current = Mournmail.message_window
104
106
  Commands.mail(run_hooks: false)
@@ -137,6 +139,7 @@ module Mournmail
137
139
  On #{mail['date']}
138
140
  #{mail['from']} wrote:
139
141
  EOF
142
+ Mournmail.insert_signature
140
143
  exchange_point_and_mark
141
144
  run_hooks(:mournmail_draft_setup_hook)
142
145
  end
@@ -373,10 +376,23 @@ module Mournmail
373
376
  end
374
377
  end
375
378
 
379
+ define_local_command(:summary_refile_spam,
380
+ doc: "Refile marked mails as spam.") do
381
+ mailbox = Mournmail.account_config[:spam_mailbox]
382
+ if mailbox.nil?
383
+ raise EditorError, "spam_mailbox is not specified"
384
+ end
385
+ summary_refile(Net::IMAP.encode_utf7(mailbox))
386
+ end
387
+
376
388
  define_local_command(:summary_prefetch,
377
389
  doc: "Prefetch mails.") do
378
390
  summary = Mournmail.current_summary
379
391
  mailbox = Mournmail.current_mailbox
392
+ spam_mailbox = Mournmail.account_config[:spam_mailbox]
393
+ if mailbox == Net::IMAP.encode_utf7(spam_mailbox)
394
+ raise EditorError, "Can't prefetch spam"
395
+ end
380
396
  target_uids = @buffer.to_s.scan(/^ *\d+/).map { |s|
381
397
  s.to_i
382
398
  }.select { |uid|
@@ -394,9 +410,7 @@ module Mournmail
394
410
  s = i.attr["BODY[]"]
395
411
  if s
396
412
  cache_id = Mournmail.write_mail_cache(s)
397
- if mailbox != Mournmail.account_config[:spam_mailbox]
398
- Mournmail.index_mail(cache_id, Mail.new(s))
399
- end
413
+ Mournmail.index_mail(cache_id, Mail.new(s))
400
414
  summary[uid].cache_id = cache_id
401
415
  end
402
416
  end
@@ -450,7 +464,7 @@ module Mournmail
450
464
 
451
465
  define_local_command(:summary_change_account,
452
466
  doc: "Change the current account.") do
453
- |account = read_account_name("Change account: ")|
467
+ |account = Mournmail.read_account_name("Change account: ")|
454
468
  unless CONFIG[:mournmail_accounts].key?(account)
455
469
  raise EditorError, "No such account: #{account}"
456
470
  end
@@ -651,13 +665,6 @@ module Mournmail
651
665
  message
652
666
  end
653
667
 
654
- def read_account_name(prompt, **opts)
655
- f = ->(s) {
656
- complete_for_minibuffer(s, CONFIG[:mournmail_accounts].keys)
657
- }
658
- read_from_minibuffer(prompt, completion_proc: f, **opts)
659
- end
660
-
661
668
  def delete_from_summary(summary, uids, msg)
662
669
  summary.delete_item_if do |item|
663
670
  uids.include?(item.uid)
@@ -225,15 +225,16 @@ module Mournmail
225
225
  end
226
226
  end
227
227
 
228
- def self.google_access_token
229
- auth_path = File.expand_path("cache/#{current_account}/google_auth.json",
228
+ def self.google_access_token(account = current_account)
229
+ auth_path = File.expand_path("cache/#{account}/google_auth.json",
230
230
  CONFIG[:mournmail_directory])
231
231
  FileUtils.mkdir_p(File.dirname(auth_path))
232
232
  store = Google::APIClient::FileStore.new(auth_path)
233
233
  storage = Google::APIClient::Storage.new(store)
234
234
  storage.authorize
235
235
  if storage.authorization.nil?
236
- path = File.expand_path(account_config[:client_secret_path])
236
+ conf = CONFIG[:mournmail_accounts][account]
237
+ path = File.expand_path(conf[:client_secret_path])
237
238
  client_secrets = Google::APIClient::ClientSecrets.load(path)
238
239
  auth_client = client_secrets.to_authorization
239
240
  auth_client.update!(
@@ -382,7 +383,8 @@ module Mournmail
382
383
  dir = File.dirname(path)
383
384
  base = File.basename(path)
384
385
  begin
385
- f = Tempfile.create(["#{base}-", ".tmp"], dir)
386
+ f = Tempfile.create(["#{base}-", ".tmp"], dir,
387
+ external_encoding: "ASCII-8BIT", binmode: true)
386
388
  begin
387
389
  f.write(s)
388
390
  ensure
@@ -554,4 +556,24 @@ module Mournmail
554
556
  def self.parse_mail(s)
555
557
  Mail.new(s.scrub("??"))
556
558
  end
559
+
560
+ def self.read_account_name(prompt, **opts)
561
+ f = ->(s) {
562
+ complete_for_minibuffer(s, CONFIG[:mournmail_accounts].keys)
563
+ }
564
+ read_from_minibuffer(prompt, completion_proc: f, **opts)
565
+ end
566
+
567
+ def self.insert_signature
568
+ account = Buffer.current[:mournmail_delivery_account] ||
569
+ Mournmail.current_account
570
+ signature = CONFIG[:mournmail_accounts][account][:signature]
571
+ if signature
572
+ Buffer.current.save_excursion do
573
+ end_of_buffer
574
+ insert("\n")
575
+ insert(signature)
576
+ end
577
+ end
578
+ end
557
579
  end
@@ -1,3 +1,3 @@
1
1
  module Mournmail
2
- VERSION = "0.3.2"
2
+ VERSION = "1.0.0"
3
3
  end
data/mournmail.gemspec CHANGED
@@ -30,5 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_runtime_dependency "html2text"
31
31
 
32
32
  spec.add_development_dependency "bundler"
33
- spec.add_development_dependency "rake", "~> 12.0"
33
+ spec.add_development_dependency "rake", ">= 12.0"
34
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mournmail
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-19 00:00:00.000000000 Z
11
+ date: 2021-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: textbringer
@@ -126,14 +126,14 @@ dependencies:
126
126
  name: rake
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - "~>"
129
+ - - ">="
130
130
  - !ruby/object:Gem::Version
131
131
  version: '12.0'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - "~>"
136
+ - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '12.0'
139
139
  description: A message user agent for Textbringer.