mournmail 1.0.4 → 2
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/.github/workflows/push_gem.yml +48 -0
- data/README.md +61 -0
- data/lib/mournmail/config.rb +15 -0
- data/lib/mournmail/draft_mode.rb +7 -3
- data/lib/mournmail/message_mode.rb +6 -4
- data/lib/mournmail/message_rendering.rb +8 -1
- data/lib/mournmail/summary.rb +10 -3
- data/lib/mournmail/summary_mode.rb +19 -8
- data/lib/mournmail/utils.rb +25 -11
- data/lib/mournmail/version.rb +1 -1
- data/mournmail.gemspec +1 -0
- metadata +18 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0470feee9eea6f5ba0071596fe11f01dace923c44ce6c1b33738d4afc7712e07
|
4
|
+
data.tar.gz: 242f831af33c0b3fb3eec8958ecb6db122d055f263452f7c4a9a3c5964db000c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aaf1a5eec30365b900c65a6d927bbcb5ae340be299190a48acb3107bc3f325e52e52b2045cea6079a315553f631fd97be4e3c1f888ff25a0ca7924975d9320df
|
7
|
+
data.tar.gz: 3e99259ee5ac139ca90fb047cab4d0121ddfcbbebb3a007ff30544da2595046178915ca54cb82a9ef458fd71ed8b4df99267ce05ea8028af15180e716e4a83e1
|
@@ -0,0 +1,48 @@
|
|
1
|
+
name: Publish gem to rubygems.org
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
tags:
|
6
|
+
- 'v*'
|
7
|
+
|
8
|
+
permissions:
|
9
|
+
contents: read
|
10
|
+
|
11
|
+
jobs:
|
12
|
+
push:
|
13
|
+
if: github.repository == 'shugo/mournmail'
|
14
|
+
runs-on: ubuntu-latest
|
15
|
+
|
16
|
+
environment:
|
17
|
+
name: rubygems.org
|
18
|
+
url: https://rubygems.org/gems/mournmail
|
19
|
+
|
20
|
+
permissions:
|
21
|
+
contents: write
|
22
|
+
id-token: write
|
23
|
+
|
24
|
+
steps:
|
25
|
+
# Set up
|
26
|
+
- name: Harden Runner
|
27
|
+
uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0
|
28
|
+
with:
|
29
|
+
egress-policy: audit
|
30
|
+
|
31
|
+
- uses: actions/checkout@v4
|
32
|
+
|
33
|
+
- name: Set up Ruby
|
34
|
+
uses: ruby/setup-ruby@v1
|
35
|
+
with:
|
36
|
+
bundler-cache: true
|
37
|
+
ruby-version: ruby
|
38
|
+
|
39
|
+
# Release
|
40
|
+
- name: Publish to RubyGems
|
41
|
+
uses: rubygems/release-gem@v1
|
42
|
+
|
43
|
+
- name: Create GitHub release
|
44
|
+
run: |
|
45
|
+
tag_name="$(git describe --tags --abbrev=0)"
|
46
|
+
gh release create "${tag_name}" --verify-tag --draft --generate-notes
|
47
|
+
env:
|
48
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
data/README.md
CHANGED
@@ -70,6 +70,67 @@ Type `M-x mail` to send a mail.
|
|
70
70
|
|
71
71
|
Type `M-x mournmail` to visit INBOX.
|
72
72
|
|
73
|
+
## Key bindings
|
74
|
+
|
75
|
+
### Summary
|
76
|
+
|
77
|
+
|Key |Command |Description |
|
78
|
+
|---|---|---|
|
79
|
+
|s |mournmail_summary_sync |Sync summary. With C-u sync all mails |
|
80
|
+
|SPC |summary_read_command |Read a mail |
|
81
|
+
|C-h |summary_scroll_down_command |Scroll down the current message |
|
82
|
+
|n |summary_next_command |Display the next mail |
|
83
|
+
|w |summary_write_command |Write a new mail |
|
84
|
+
|a |summary_reply_command |Reply to the current message |
|
85
|
+
|A |summary_reply_command |Reply to the current message |
|
86
|
+
|f |summary_forward_command |Forward the current message |
|
87
|
+
|u |summary_toggle_seen_command |Toggle Seen |
|
88
|
+
|$ |summary_toggle_flagged_command |Toggle Flagged |
|
89
|
+
|d |summary_toggle_deleted_command |Toggle Deleted |
|
90
|
+
|x |summary_toggle_mark_command |Toggle mark |
|
91
|
+
|* a |summary_mark_all_command |Mark all mails |
|
92
|
+
|* n |summary_unmark_all_command |Unmark all mails |
|
93
|
+
|* r |summary_mark_read_command |Mark read mails |
|
94
|
+
|* u |summary_mark_unread_command |Mark unread mails |
|
95
|
+
|* s |summary_mark_flagged_command |Mark flagged mails |
|
96
|
+
|* t |summary_mark_unflagged_command |Mark unflagged mails |
|
97
|
+
|y |summary_archive_command |Archive mails. Archived mails will be deleted or refiled from the server, and only shown by summary_search_command |
|
98
|
+
|o |summary_refile_command |Refile marked mails |
|
99
|
+
|! |summary_refile_spam_command |Refile marked mails as spam |
|
100
|
+
|p |summary_prefetch_command |Prefetch mails |
|
101
|
+
|X |summary_expunge_command |Expunge deleted mails |
|
102
|
+
|v |summary_view_source_command |View source of a mail |
|
103
|
+
|M |summary_merge_partial_command |Merge marked message/partial |
|
104
|
+
|q |mournmail_quit |Quit Mournmail |
|
105
|
+
|k |previous_line |Move up |
|
106
|
+
|j |next_line |Move down |
|
107
|
+
|m |mournmail_visit_mailbox |Visit mailbox |
|
108
|
+
|S |mournmail_visit_spam_mailbox |Visit spam mailbox |
|
109
|
+
|/ |summary_search_command |Search mails |
|
110
|
+
|t |summary_show_thread_command |Show the thread of the current mail |
|
111
|
+
|@ |summary_change_account_command |Change the current account |
|
112
|
+
|
113
|
+
### Message
|
114
|
+
|
115
|
+
|Key |Command |Description |
|
116
|
+
|---|---|---|
|
117
|
+
|RET |message_open_link_or_part_command |Open link or MIME part |
|
118
|
+
|s |message_save_part_command |Save the MIME part |
|
119
|
+
|TAB |message_next_link_or_part_command| Go to the next link or MIME part |
|
120
|
+
|
121
|
+
### Draft
|
122
|
+
|
123
|
+
|Key |Command |Description |
|
124
|
+
|---|---|---|
|
125
|
+
|C-c C-c |draft_send_command |Send a mail |
|
126
|
+
|C-c C-k |draft_kill_command |Kill the draft buffer |
|
127
|
+
|C-c C-x TAB |draft_attach_file_command |Attach a file |
|
128
|
+
|C-c C-x v |draft_pgp_sign_command |PGP sign |
|
129
|
+
|C-c C-x e |draft_pgp_encrypt_command |PGP encrypt |
|
130
|
+
|C-c TAB |insert_signature_command |Insert signature |
|
131
|
+
|C-c @ |draft_change_account_command |Change account |
|
132
|
+
|TAB |draft_complete_or_insert_tab_command |Complete a mail address or insert a tab |
|
133
|
+
|
73
134
|
## Development
|
74
135
|
|
75
136
|
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/mournmail/config.rb
CHANGED
@@ -37,6 +37,7 @@ module Textbringer
|
|
37
37
|
end
|
38
38
|
CONFIG[:mournmail_addresses_path] = File.expand_path("~/.addresses")
|
39
39
|
CONFIG[:mournmail_signature_regexp] = /^-- /
|
40
|
+
CONFIG[:mournmail_summary_lines] = 7
|
40
41
|
CONFIG[:mournmail_allowed_attachment_extensions] = [
|
41
42
|
"txt",
|
42
43
|
"md",
|
@@ -54,4 +55,18 @@ module Textbringer
|
|
54
55
|
"ppt",
|
55
56
|
"zip"
|
56
57
|
]
|
58
|
+
CONFIG[:mournmail_forgotten_attachment_re] =
|
59
|
+
Regexp.new(
|
60
|
+
"^(?!>).*" +
|
61
|
+
Regexp.union(
|
62
|
+
/I('ve| have) (attached|included)/,
|
63
|
+
/See the (attached|attachment)/,
|
64
|
+
/Attached file/,
|
65
|
+
/添付(する|した|します|しました|いたします|いたしました)/,
|
66
|
+
/ファイルを参照/
|
67
|
+
).to_s
|
68
|
+
)
|
69
|
+
CONFIG[:mournmail_summary_line_limit] = 78
|
70
|
+
CONFIG[:mournmail_summary_from_limit] = 16
|
71
|
+
CONFIG[:mournmail_summary_use_line_cache] = true
|
57
72
|
end
|
data/lib/mournmail/draft_mode.rb
CHANGED
@@ -21,11 +21,15 @@ module Mournmail
|
|
21
21
|
|
22
22
|
define_local_command(:draft_send,
|
23
23
|
doc: "Send a mail and exit from mail buffer.") do
|
24
|
-
|
25
|
-
|
24
|
+
s = @buffer.to_s
|
25
|
+
if s.match?(CONFIG[:mournmail_forgotten_attachment_re]) &&
|
26
|
+
!s.match?(/^Attached-File:/)
|
27
|
+
msg = "It seems like you forgot to attach a file. Send anyway?"
|
28
|
+
return unless yes_or_no?(msg)
|
29
|
+
else
|
30
|
+
return unless y_or_n?("Send this mail?")
|
26
31
|
end
|
27
32
|
run_hooks(:mournmail_pre_send_hook)
|
28
|
-
s = @buffer.to_s
|
29
33
|
charset = CONFIG[:mournmail_charset]
|
30
34
|
begin
|
31
35
|
s.encode(charset)
|
@@ -11,10 +11,12 @@ module Mournmail
|
|
11
11
|
MESSAGE_MODE_MAP.define_key("\t", :message_next_link_or_part_command)
|
12
12
|
|
13
13
|
# See http://nihongo.jp/support/mail_guide/dev_guide.txt
|
14
|
-
|
15
|
-
|
14
|
+
URI_REGEXP = Regexp.union(URI.regexp("http"),
|
15
|
+
URI.regexp("https"),
|
16
|
+
URI.regexp("ftp"),
|
17
|
+
URI.regexp("mailto"))
|
16
18
|
MIME_REGEXP = /^\[(([0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*|PGP\/MIME .*)\]$/
|
17
|
-
URI_OR_MIME_REGEXP =
|
19
|
+
URI_OR_MIME_REGEXP = Regexp.union(URI_REGEXP, MIME_REGEXP)
|
18
20
|
|
19
21
|
define_syntax :field_name, /^[A-Za-z\-]+: /
|
20
22
|
define_syntax :quotation, /^>.*/
|
@@ -128,7 +130,7 @@ module Mournmail
|
|
128
130
|
if part.multipart?
|
129
131
|
raise EditorError, "Can't open a multipart entity."
|
130
132
|
end
|
131
|
-
ext = part_file_name(part).slice(/\.([^.]+)\z/, 1)
|
133
|
+
ext = part_file_name(part).slice(/\.([^.]+)\z/, 1).downcase
|
132
134
|
if part.main_type != "text" || part.sub_type == "html"
|
133
135
|
if ext.nil?
|
134
136
|
raise EditorError, "The extension of the filename is not specified"
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "mail"
|
2
|
+
require "nokogiri"
|
2
3
|
require "html2text"
|
3
4
|
|
4
5
|
module Mournmail
|
@@ -34,7 +35,9 @@ module Mournmail
|
|
34
35
|
elsif main_type.nil? || main_type == "text"
|
35
36
|
s = Mournmail.to_utf8(body.decoded, charset)
|
36
37
|
if sub_type == "html"
|
37
|
-
|
38
|
+
doc = Nokogiri::HTML(s)
|
39
|
+
doc.css("script, style, link").each { |node| node.remove }
|
40
|
+
"[0 text/html]\n" + doc.css("body").text.squeeze(" \n")
|
38
41
|
else
|
39
42
|
s
|
40
43
|
end
|
@@ -115,6 +118,10 @@ module Mournmail
|
|
115
118
|
type = Mail::Encodings.decode_encode(self["content-type"].to_s,
|
116
119
|
:decode) rescue
|
117
120
|
"broken/type; error=\"#{$!} (#{$!.class})\""
|
121
|
+
filename = self["content-disposition"]&.filename
|
122
|
+
if filename && !self["content-type"]&.filename
|
123
|
+
type += "; filename=#{filename}"
|
124
|
+
end
|
118
125
|
"[#{index} #{type}]\n" +
|
119
126
|
render_content(indices, no_content)
|
120
127
|
end
|
data/lib/mournmail/summary.rb
CHANGED
@@ -213,8 +213,12 @@ module Mournmail
|
|
213
213
|
}
|
214
214
|
end
|
215
215
|
|
216
|
-
def to_s(limit =
|
217
|
-
|
216
|
+
def to_s(limit = CONFIG[:mournmail_summary_line_limit],
|
217
|
+
from_limit = CONFIG[:mournmail_summary_from_limit],
|
218
|
+
level = 0)
|
219
|
+
if @line.nil? || !CONFIG[:mournmail_summary_use_line_cache]
|
220
|
+
@line = format_line(limit, from_limit, level)
|
221
|
+
end
|
218
222
|
return @line if @replies.empty?
|
219
223
|
s = @line.dup
|
220
224
|
child_level = level + 1
|
@@ -247,7 +251,9 @@ module Mournmail
|
|
247
251
|
|
248
252
|
private
|
249
253
|
|
250
|
-
def format_line(limit =
|
254
|
+
def format_line(limit = CONFIG[:mournmail_summary_line_limit],
|
255
|
+
from_limit = CONFIG[:mournmail_summary_from_limit],
|
256
|
+
level = 0)
|
251
257
|
space = " " * (level < 8 ? level : 8)
|
252
258
|
s = +""
|
253
259
|
s << format("%6d %s%s %s[ %s ] ",
|
@@ -262,6 +268,7 @@ module Mournmail
|
|
262
268
|
width = 0
|
263
269
|
str = +""
|
264
270
|
s.each_char do |c|
|
271
|
+
next if c == "\n"
|
265
272
|
w = Buffer.display_width(c)
|
266
273
|
width += w
|
267
274
|
if width > n
|
@@ -390,7 +390,7 @@ module Mournmail
|
|
390
390
|
summary = Mournmail.current_summary
|
391
391
|
mailbox = Mournmail.current_mailbox
|
392
392
|
spam_mailbox = Mournmail.account_config[:spam_mailbox]
|
393
|
-
if mailbox == Net::IMAP.encode_utf7(spam_mailbox)
|
393
|
+
if spam_mailbox && mailbox == Net::IMAP.encode_utf7(spam_mailbox)
|
394
394
|
raise EditorError, "Can't prefetch spam"
|
395
395
|
end
|
396
396
|
target_uids = @buffer.to_s.scan(/^ *\d+/).map { |s|
|
@@ -478,6 +478,11 @@ module Mournmail
|
|
478
478
|
|
479
479
|
private
|
480
480
|
|
481
|
+
def get_summary_item(uid)
|
482
|
+
summary = Mournmail.current_summary
|
483
|
+
summary && summary[uid]
|
484
|
+
end
|
485
|
+
|
481
486
|
def selected_uid
|
482
487
|
uid = @buffer.save_excursion {
|
483
488
|
@buffer.beginning_of_line
|
@@ -538,7 +543,7 @@ module Mournmail
|
|
538
543
|
end
|
539
544
|
|
540
545
|
def mark_as_seen(uid, update_server)
|
541
|
-
summary_item =
|
546
|
+
summary_item = get_summary_item(uid)
|
542
547
|
if summary_item && !summary_item.flags.include?(:Seen)
|
543
548
|
summary_item.set_flag(:Seen, update_server: update_server)
|
544
549
|
Mournmail.current_summary.save
|
@@ -547,7 +552,7 @@ module Mournmail
|
|
547
552
|
end
|
548
553
|
|
549
554
|
def toggle_flag(uid, flag)
|
550
|
-
summary_item =
|
555
|
+
summary_item = get_summary_item(uid)
|
551
556
|
if summary_item
|
552
557
|
Mournmail.background do
|
553
558
|
summary_item.toggle_flag(flag)
|
@@ -594,10 +599,16 @@ module Mournmail
|
|
594
599
|
def show_search_result(messages,
|
595
600
|
query: nil, buffer_name: "*search result*")
|
596
601
|
summary_text = messages.map { |m|
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
602
|
+
s = +""
|
603
|
+
s << format("%s [ %s ] ",
|
604
|
+
m.date.strftime("%m/%d %H:%M"),
|
605
|
+
ljust(m.from.to_s.gsub(/\n/, ""),
|
606
|
+
CONFIG[:mournmail_summary_from_limit]))
|
607
|
+
s << ljust(m.subject.to_s.gsub(/\n/, ""),
|
608
|
+
CONFIG[:mournmail_summary_line_limit] - Buffer.display_width(s))
|
609
|
+
s << "\n"
|
610
|
+
s
|
611
|
+
|
601
612
|
}.join
|
602
613
|
buffer = Buffer.find_or_new(buffer_name, undo_limit: 0,
|
603
614
|
read_only: true)
|
@@ -657,7 +668,7 @@ module Mournmail
|
|
657
668
|
|
658
669
|
def current_message
|
659
670
|
uid = selected_uid
|
660
|
-
item =
|
671
|
+
item = get_summary_item(uid)
|
661
672
|
message = Groonga["Messages"][item.cache_id]
|
662
673
|
if message.nil?
|
663
674
|
raise EditorError, "No message found"
|
data/lib/mournmail/utils.rb
CHANGED
@@ -13,15 +13,28 @@ require 'google/api_client/auth/storages/file_store'
|
|
13
13
|
require 'launchy'
|
14
14
|
require "socket"
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
16
|
+
if defined?(Net::SMTP::Authenticator)
|
17
|
+
class Net::SMTP
|
18
|
+
class AuthXOAuth2 < Net::SMTP::Authenticator
|
19
|
+
auth_type :xoauth2
|
20
|
+
|
21
|
+
def auth(user, secret)
|
22
|
+
s = Net::IMAP::XOauth2Authenticator.new(user, secret).process("")
|
23
|
+
finish('AUTH XOAUTH2 ' + base64_encode(s))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
else
|
28
|
+
class Net::SMTP
|
29
|
+
def auth_xoauth2(user, secret)
|
30
|
+
check_auth_args user, secret
|
31
|
+
res = critical {
|
32
|
+
s = Net::IMAP::XOauth2Authenticator.new(user, secret).process("")
|
33
|
+
get_response('AUTH XOAUTH2 ' + base64_encode(s))
|
34
|
+
}
|
35
|
+
check_auth_response res
|
36
|
+
res
|
37
|
+
end
|
25
38
|
end
|
26
39
|
end
|
27
40
|
|
@@ -116,7 +129,8 @@ module Mournmail
|
|
116
129
|
def self.message_window
|
117
130
|
if Window.list.size == 1
|
118
131
|
split_window
|
119
|
-
|
132
|
+
n = Window.current.lines - (CONFIG[:mournmail_summary_lines] + 1)
|
133
|
+
shrink_window(n)
|
120
134
|
end
|
121
135
|
windows = Window.list
|
122
136
|
i = (windows.index(Window.current) + 1) % windows.size
|
@@ -184,7 +198,7 @@ module Mournmail
|
|
184
198
|
end
|
185
199
|
Timeout.timeout(CONFIG[:mournmail_imap_connect_timeout]) do
|
186
200
|
@imap = Net::IMAP.new(conf[:imap_host],
|
187
|
-
conf[:imap_options])
|
201
|
+
conf[:imap_options].except(:auth_type, :user_name, :password))
|
188
202
|
@imap.authenticate(auth_type, conf[:imap_options][:user_name],
|
189
203
|
password)
|
190
204
|
@mailboxes = @imap.list("", "*").map { |mbox|
|
data/lib/mournmail/version.rb
CHANGED
data/mournmail.gemspec
CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_runtime_dependency "rroonga"
|
30
30
|
spec.add_runtime_dependency "google-apis-core"
|
31
31
|
spec.add_runtime_dependency "launchy"
|
32
|
+
spec.add_runtime_dependency "nokogiri"
|
32
33
|
spec.add_runtime_dependency "html2text"
|
33
34
|
|
34
35
|
spec.add_development_dependency "bundler"
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mournmail
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: '2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shugo Maeda
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: textbringer
|
@@ -122,6 +121,20 @@ dependencies:
|
|
122
121
|
- - ">="
|
123
122
|
- !ruby/object:Gem::Version
|
124
123
|
version: '0'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: nokogiri
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
type: :runtime
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
125
138
|
- !ruby/object:Gem::Dependency
|
126
139
|
name: html2text
|
127
140
|
requirement: !ruby/object:Gem::Requirement
|
@@ -172,6 +185,7 @@ executables:
|
|
172
185
|
extensions: []
|
173
186
|
extra_rdoc_files: []
|
174
187
|
files:
|
188
|
+
- ".github/workflows/push_gem.yml"
|
175
189
|
- ".gitignore"
|
176
190
|
- Gemfile
|
177
191
|
- LICENSE.txt
|
@@ -199,7 +213,6 @@ homepage: https://github.com/shugo/mournmail
|
|
199
213
|
licenses:
|
200
214
|
- MIT
|
201
215
|
metadata: {}
|
202
|
-
post_install_message:
|
203
216
|
rdoc_options: []
|
204
217
|
require_paths:
|
205
218
|
- lib
|
@@ -214,8 +227,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
214
227
|
- !ruby/object:Gem::Version
|
215
228
|
version: '0'
|
216
229
|
requirements: []
|
217
|
-
rubygems_version: 3.
|
218
|
-
signing_key:
|
230
|
+
rubygems_version: 3.6.7
|
219
231
|
specification_version: 4
|
220
232
|
summary: A message user agent for Textbringer.
|
221
233
|
test_files: []
|