mxctl 0.3.0__py3-none-any.whl
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.
- mxctl/__init__.py +3 -0
- mxctl/__main__.py +5 -0
- mxctl/commands/__init__.py +0 -0
- mxctl/commands/mail/__init__.py +1 -0
- mxctl/commands/mail/accounts.py +347 -0
- mxctl/commands/mail/actions.py +593 -0
- mxctl/commands/mail/ai.py +355 -0
- mxctl/commands/mail/analytics.py +390 -0
- mxctl/commands/mail/attachments.py +156 -0
- mxctl/commands/mail/batch.py +402 -0
- mxctl/commands/mail/compose.py +133 -0
- mxctl/commands/mail/composite.py +430 -0
- mxctl/commands/mail/inbox_tools.py +502 -0
- mxctl/commands/mail/manage.py +185 -0
- mxctl/commands/mail/messages.py +363 -0
- mxctl/commands/mail/setup.py +362 -0
- mxctl/commands/mail/system.py +202 -0
- mxctl/commands/mail/templates.py +145 -0
- mxctl/commands/mail/todoist_integration.py +145 -0
- mxctl/commands/mail/undo.py +323 -0
- mxctl/config.py +227 -0
- mxctl/main.py +89 -0
- mxctl/util/__init__.py +0 -0
- mxctl/util/applescript.py +139 -0
- mxctl/util/applescript_templates.py +185 -0
- mxctl/util/dates.py +54 -0
- mxctl/util/formatting.py +52 -0
- mxctl/util/mail_helpers.py +192 -0
- mxctl-0.3.0.dist-info/METADATA +439 -0
- mxctl-0.3.0.dist-info/RECORD +33 -0
- mxctl-0.3.0.dist-info/WHEEL +4 -0
- mxctl-0.3.0.dist-info/entry_points.txt +2 -0
- mxctl-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Message listing and reading commands: list, read, search."""
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
from mxctl.config import (
|
|
6
|
+
DEFAULT_BODY_LENGTH,
|
|
7
|
+
DEFAULT_MESSAGE_LIMIT,
|
|
8
|
+
FIELD_SEPARATOR,
|
|
9
|
+
resolve_account,
|
|
10
|
+
save_message_aliases,
|
|
11
|
+
validate_limit,
|
|
12
|
+
)
|
|
13
|
+
from mxctl.util.applescript import escape, run, validate_msg_id
|
|
14
|
+
from mxctl.util.dates import parse_date, to_applescript_date
|
|
15
|
+
from mxctl.util.formatting import format_output, truncate
|
|
16
|
+
from mxctl.util.mail_helpers import parse_message_line, resolve_mailbox, resolve_message_context
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# list
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def cmd_list(args) -> None:
|
|
23
|
+
"""List messages in a mailbox with optional filtering."""
|
|
24
|
+
account, mailbox, acct_escaped, mb_escaped = resolve_message_context(args)
|
|
25
|
+
limit = validate_limit(getattr(args, "limit", DEFAULT_MESSAGE_LIMIT))
|
|
26
|
+
unread_only = getattr(args, "unread", False)
|
|
27
|
+
after = getattr(args, "after", None)
|
|
28
|
+
before = getattr(args, "before", None)
|
|
29
|
+
|
|
30
|
+
filters = []
|
|
31
|
+
if unread_only:
|
|
32
|
+
filters.append("read status is false")
|
|
33
|
+
if after:
|
|
34
|
+
start_dt = parse_date(after)
|
|
35
|
+
filters.append(f'date received >= date "{to_applescript_date(start_dt)}"')
|
|
36
|
+
if before:
|
|
37
|
+
end_dt = parse_date(before) + timedelta(days=1)
|
|
38
|
+
filters.append(f'date received < date "{to_applescript_date(end_dt)}"')
|
|
39
|
+
|
|
40
|
+
filter_clause = " and ".join(filters) if filters else ""
|
|
41
|
+
whose_clause = f"whose {filter_clause}" if filter_clause else ""
|
|
42
|
+
|
|
43
|
+
script = f"""
|
|
44
|
+
tell application "Mail"
|
|
45
|
+
set mb to mailbox "{mb_escaped}" of account "{acct_escaped}"
|
|
46
|
+
set allMsgs to (every message of mb {whose_clause})
|
|
47
|
+
set msgCount to count of allMsgs
|
|
48
|
+
set actualLimit to {limit}
|
|
49
|
+
if msgCount < actualLimit then set actualLimit to msgCount
|
|
50
|
+
if actualLimit = 0 then return ""
|
|
51
|
+
set output to ""
|
|
52
|
+
repeat with i from 1 to actualLimit
|
|
53
|
+
set m to item i of allMsgs
|
|
54
|
+
set output to output & (id of m) & "{FIELD_SEPARATOR}" & (subject of m) & "{FIELD_SEPARATOR}" & (sender of m) & "{FIELD_SEPARATOR}" & (date received of m) & "{FIELD_SEPARATOR}" & (read status of m) & "{FIELD_SEPARATOR}" & (flagged status of m) & linefeed
|
|
55
|
+
end repeat
|
|
56
|
+
return output
|
|
57
|
+
end tell
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
result = run(script)
|
|
61
|
+
|
|
62
|
+
if not result.strip():
|
|
63
|
+
filter_desc = []
|
|
64
|
+
if unread_only:
|
|
65
|
+
filter_desc.append("unread")
|
|
66
|
+
if after:
|
|
67
|
+
filter_desc.append(f"from {after}")
|
|
68
|
+
if before:
|
|
69
|
+
filter_desc.append(f"to {before}")
|
|
70
|
+
filter_str = f" ({', '.join(filter_desc)})" if filter_desc else ""
|
|
71
|
+
format_output(args, f"No messages found in {mailbox}{filter_str}.")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# Build JSON data and text output
|
|
75
|
+
messages = []
|
|
76
|
+
for line in result.strip().split("\n"):
|
|
77
|
+
if not line.strip():
|
|
78
|
+
continue
|
|
79
|
+
msg = parse_message_line(line, ["id", "subject", "sender", "date", "read", "flagged"], FIELD_SEPARATOR)
|
|
80
|
+
if msg is not None:
|
|
81
|
+
messages.append(msg)
|
|
82
|
+
|
|
83
|
+
save_message_aliases([m["id"] for m in messages])
|
|
84
|
+
for i, m in enumerate(messages, 1):
|
|
85
|
+
m["alias"] = i
|
|
86
|
+
|
|
87
|
+
text = f"Messages in {mailbox} [{account}] (showing up to {limit}):"
|
|
88
|
+
for m in messages:
|
|
89
|
+
status_icons = []
|
|
90
|
+
if not m["read"]:
|
|
91
|
+
status_icons.append("UNREAD")
|
|
92
|
+
if m["flagged"]:
|
|
93
|
+
status_icons.append("FLAGGED")
|
|
94
|
+
status_str = f" [{', '.join(status_icons)}]" if status_icons else ""
|
|
95
|
+
text += f"\n- [{m['alias']}] {truncate(m['subject'], 60)}{status_str}"
|
|
96
|
+
text += f"\n From: {m['sender']}"
|
|
97
|
+
text += f"\n Date: {m['date']}"
|
|
98
|
+
format_output(args, text, json_data=messages)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# read
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def cmd_read(args) -> None:
|
|
106
|
+
"""Read full message details including headers and body."""
|
|
107
|
+
account, mailbox, acct_escaped, mb_escaped = resolve_message_context(args)
|
|
108
|
+
message_id = validate_msg_id(args.id)
|
|
109
|
+
short = getattr(args, "short", False)
|
|
110
|
+
body_limit = DEFAULT_BODY_LENGTH if not short else 500
|
|
111
|
+
|
|
112
|
+
script = f"""
|
|
113
|
+
tell application "Mail"
|
|
114
|
+
set mb to mailbox "{mb_escaped}" of account "{acct_escaped}"
|
|
115
|
+
set theMsg to first message of mb whose id is {message_id}
|
|
116
|
+
|
|
117
|
+
set msgId to id of theMsg
|
|
118
|
+
set msgMessageId to message id of theMsg
|
|
119
|
+
set msgSubject to subject of theMsg
|
|
120
|
+
set msgSender to sender of theMsg
|
|
121
|
+
set msgDate to date received of theMsg
|
|
122
|
+
set msgRead to read status of theMsg
|
|
123
|
+
set msgFlagged to flagged status of theMsg
|
|
124
|
+
set msgJunk to junk mail status of theMsg
|
|
125
|
+
set msgDeleted to deleted status of theMsg
|
|
126
|
+
set msgForwarded to was forwarded of theMsg
|
|
127
|
+
set msgReplied to was replied to of theMsg
|
|
128
|
+
|
|
129
|
+
set toList to ""
|
|
130
|
+
repeat with r in (to recipients of theMsg)
|
|
131
|
+
set toList to toList & (address of r) & ","
|
|
132
|
+
end repeat
|
|
133
|
+
|
|
134
|
+
set ccList to ""
|
|
135
|
+
repeat with r in (cc recipients of theMsg)
|
|
136
|
+
set ccList to ccList & (address of r) & ","
|
|
137
|
+
end repeat
|
|
138
|
+
|
|
139
|
+
try
|
|
140
|
+
set msgReplyTo to reply to of theMsg
|
|
141
|
+
on error
|
|
142
|
+
set msgReplyTo to ""
|
|
143
|
+
end try
|
|
144
|
+
|
|
145
|
+
set msgContent to content of theMsg
|
|
146
|
+
set attCount to count of mail attachments of theMsg
|
|
147
|
+
|
|
148
|
+
return (msgId as text) & "{FIELD_SEPARATOR}" & msgMessageId & "{FIELD_SEPARATOR}" & msgSubject & "{FIELD_SEPARATOR}" & msgSender & "{FIELD_SEPARATOR}" & (msgDate as text) & "{FIELD_SEPARATOR}" & (msgRead as text) & "{FIELD_SEPARATOR}" & (msgFlagged as text) & "{FIELD_SEPARATOR}" & (msgJunk as text) & "{FIELD_SEPARATOR}" & (msgDeleted as text) & "{FIELD_SEPARATOR}" & (msgForwarded as text) & "{FIELD_SEPARATOR}" & (msgReplied as text) & "{FIELD_SEPARATOR}" & toList & "{FIELD_SEPARATOR}" & ccList & "{FIELD_SEPARATOR}" & msgReplyTo & "{FIELD_SEPARATOR}" & msgContent & "{FIELD_SEPARATOR}" & (attCount as text)
|
|
149
|
+
end tell
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
result = run(script)
|
|
153
|
+
parts = result.split(FIELD_SEPARATOR)
|
|
154
|
+
|
|
155
|
+
if len(parts) < 16:
|
|
156
|
+
format_output(args, f"Message details: {result}")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
(
|
|
160
|
+
msg_id, message_id_header, subject, sender, date,
|
|
161
|
+
read, flagged, junk, deleted, forwarded, replied,
|
|
162
|
+
to_list, cc_list, reply_to, content, att_count,
|
|
163
|
+
) = parts[:16]
|
|
164
|
+
|
|
165
|
+
# U+FFFC (object replacement character) appears where HTML emails embed
|
|
166
|
+
# inline images. Replace with a readable placeholder.
|
|
167
|
+
content = content.replace("\ufffc", "[image]")
|
|
168
|
+
|
|
169
|
+
# Build JSON data
|
|
170
|
+
data = {
|
|
171
|
+
"id": int(msg_id) if msg_id.isdigit() else msg_id,
|
|
172
|
+
"message_id": message_id_header,
|
|
173
|
+
"account": account,
|
|
174
|
+
"mailbox": mailbox,
|
|
175
|
+
"subject": subject,
|
|
176
|
+
"from": sender,
|
|
177
|
+
"to": [a.strip() for a in to_list.rstrip(",").split(",") if a.strip()],
|
|
178
|
+
"cc": [a.strip() for a in cc_list.rstrip(",").split(",") if a.strip()],
|
|
179
|
+
"reply_to": reply_to or None,
|
|
180
|
+
"date": date,
|
|
181
|
+
"read": read.lower() == "true",
|
|
182
|
+
"flagged": flagged.lower() == "true",
|
|
183
|
+
"junk": junk.lower() == "true",
|
|
184
|
+
"deleted": deleted.lower() == "true",
|
|
185
|
+
"forwarded": forwarded.lower() == "true",
|
|
186
|
+
"replied": replied.lower() == "true",
|
|
187
|
+
"attachments": int(att_count) if att_count.isdigit() else 0,
|
|
188
|
+
"body": truncate(content, body_limit),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Build text output
|
|
192
|
+
text = f"Message Details:\nID: {msg_id}\nMessage-ID: {message_id_header}"
|
|
193
|
+
text += f"\nAccount: {account}\nMailbox: {mailbox}"
|
|
194
|
+
text += f"\n\nSubject: {subject}\nFrom: {sender}\nTo: {to_list.rstrip(',')}"
|
|
195
|
+
if cc_list.strip(","):
|
|
196
|
+
text += f"\nCC: {cc_list.rstrip(',')}"
|
|
197
|
+
if reply_to:
|
|
198
|
+
text += f"\nReply-To: {reply_to}"
|
|
199
|
+
text += f"\nDate: {date}"
|
|
200
|
+
text += "\n\nStatus:"
|
|
201
|
+
text += f"\n Read: {read} Flagged: {flagged} Junk: {junk}"
|
|
202
|
+
text += f"\n Forwarded: {forwarded} Replied: {replied}"
|
|
203
|
+
text += f"\n\nAttachments: {att_count}"
|
|
204
|
+
text += f"\n\n--- Body ---\n{truncate(content, body_limit)}"
|
|
205
|
+
format_output(args, text, json_data=data)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# search
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def cmd_search(args) -> None:
|
|
213
|
+
"""Search messages by subject or sender."""
|
|
214
|
+
query = args.query
|
|
215
|
+
field = "sender" if getattr(args, "sender", False) else "subject"
|
|
216
|
+
account = resolve_account(getattr(args, "account", None))
|
|
217
|
+
mailbox = getattr(args, "mailbox", None)
|
|
218
|
+
limit = validate_limit(getattr(args, "limit", DEFAULT_MESSAGE_LIMIT))
|
|
219
|
+
|
|
220
|
+
query_escaped = escape(query)
|
|
221
|
+
if mailbox and account:
|
|
222
|
+
mailbox = resolve_mailbox(account, mailbox)
|
|
223
|
+
|
|
224
|
+
if mailbox and account:
|
|
225
|
+
acct_escaped = escape(account)
|
|
226
|
+
mb_escaped = escape(mailbox)
|
|
227
|
+
script = f"""
|
|
228
|
+
tell application "Mail"
|
|
229
|
+
set mb to mailbox "{mb_escaped}" of account "{acct_escaped}"
|
|
230
|
+
set searchResults to (every message of mb whose {field} contains "{query_escaped}")
|
|
231
|
+
set resultCount to count of searchResults
|
|
232
|
+
set actualLimit to {limit}
|
|
233
|
+
if resultCount < actualLimit then set actualLimit to resultCount
|
|
234
|
+
if actualLimit = 0 then return ""
|
|
235
|
+
set output to ""
|
|
236
|
+
repeat with i from 1 to actualLimit
|
|
237
|
+
set m to item i of searchResults
|
|
238
|
+
set output to output & (id of m) & "{FIELD_SEPARATOR}" & (subject of m) & "{FIELD_SEPARATOR}" & (sender of m) & "{FIELD_SEPARATOR}" & (date received of m) & "{FIELD_SEPARATOR}" & (read status of m) & "{FIELD_SEPARATOR}" & (flagged status of m) & "{FIELD_SEPARATOR}" & "{mb_escaped}" & "{FIELD_SEPARATOR}" & "{acct_escaped}" & linefeed
|
|
239
|
+
end repeat
|
|
240
|
+
return output
|
|
241
|
+
end tell
|
|
242
|
+
"""
|
|
243
|
+
elif account:
|
|
244
|
+
acct_escaped = escape(account)
|
|
245
|
+
script = f"""
|
|
246
|
+
tell application "Mail"
|
|
247
|
+
set acct to account "{acct_escaped}"
|
|
248
|
+
set output to ""
|
|
249
|
+
set totalFound to 0
|
|
250
|
+
repeat with mb in (every mailbox of acct)
|
|
251
|
+
if totalFound >= {limit} then exit repeat
|
|
252
|
+
set mbName to name of mb
|
|
253
|
+
set searchResults to (every message of mb whose {field} contains "{query_escaped}")
|
|
254
|
+
repeat with m in searchResults
|
|
255
|
+
if totalFound >= {limit} then exit repeat
|
|
256
|
+
set output to output & (id of m) & "{FIELD_SEPARATOR}" & (subject of m) & "{FIELD_SEPARATOR}" & (sender of m) & "{FIELD_SEPARATOR}" & (date received of m) & "{FIELD_SEPARATOR}" & (read status of m) & "{FIELD_SEPARATOR}" & (flagged status of m) & "{FIELD_SEPARATOR}" & mbName & "{FIELD_SEPARATOR}" & "{acct_escaped}" & linefeed
|
|
257
|
+
set totalFound to totalFound + 1
|
|
258
|
+
end repeat
|
|
259
|
+
end repeat
|
|
260
|
+
return output
|
|
261
|
+
end tell
|
|
262
|
+
"""
|
|
263
|
+
else:
|
|
264
|
+
script = f"""
|
|
265
|
+
tell application "Mail"
|
|
266
|
+
set output to ""
|
|
267
|
+
set totalFound to 0
|
|
268
|
+
repeat with acct in (every account)
|
|
269
|
+
if totalFound >= {limit} then exit repeat
|
|
270
|
+
set acctName to name of acct
|
|
271
|
+
repeat with mb in (every mailbox of acct)
|
|
272
|
+
if totalFound >= {limit} then exit repeat
|
|
273
|
+
set mbName to name of mb
|
|
274
|
+
set searchResults to (every message of mb whose {field} contains "{query_escaped}")
|
|
275
|
+
repeat with m in searchResults
|
|
276
|
+
if totalFound >= {limit} then exit repeat
|
|
277
|
+
set output to output & (id of m) & "{FIELD_SEPARATOR}" & (subject of m) & "{FIELD_SEPARATOR}" & (sender of m) & "{FIELD_SEPARATOR}" & (date received of m) & "{FIELD_SEPARATOR}" & (read status of m) & "{FIELD_SEPARATOR}" & (flagged status of m) & "{FIELD_SEPARATOR}" & mbName & "{FIELD_SEPARATOR}" & acctName & linefeed
|
|
278
|
+
set totalFound to totalFound + 1
|
|
279
|
+
end repeat
|
|
280
|
+
end repeat
|
|
281
|
+
end repeat
|
|
282
|
+
return output
|
|
283
|
+
end tell
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
result = run(script)
|
|
287
|
+
|
|
288
|
+
if not result.strip():
|
|
289
|
+
scope = ""
|
|
290
|
+
if mailbox and account:
|
|
291
|
+
scope = f" in {mailbox} [{account}]"
|
|
292
|
+
elif account:
|
|
293
|
+
scope = f" in {account}"
|
|
294
|
+
format_output(args, f"No messages found matching '{query}' in {field}{scope}.")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
# Build JSON data and text output
|
|
298
|
+
messages = []
|
|
299
|
+
for line in result.strip().split("\n"):
|
|
300
|
+
if not line.strip():
|
|
301
|
+
continue
|
|
302
|
+
msg = parse_message_line(
|
|
303
|
+
line,
|
|
304
|
+
["id", "subject", "sender", "date", "read", "flagged", "mailbox", "account"],
|
|
305
|
+
FIELD_SEPARATOR,
|
|
306
|
+
)
|
|
307
|
+
if msg is not None:
|
|
308
|
+
messages.append(msg)
|
|
309
|
+
|
|
310
|
+
save_message_aliases([m["id"] for m in messages])
|
|
311
|
+
for i, m in enumerate(messages, 1):
|
|
312
|
+
m["alias"] = i
|
|
313
|
+
|
|
314
|
+
text = f"Search results for '{query}' in {field} (up to {limit}):"
|
|
315
|
+
for m in messages:
|
|
316
|
+
status_icons = []
|
|
317
|
+
if not m["read"]:
|
|
318
|
+
status_icons.append("UNREAD")
|
|
319
|
+
if m["flagged"]:
|
|
320
|
+
status_icons.append("FLAGGED")
|
|
321
|
+
status_str = f" [{', '.join(status_icons)}]" if status_icons else ""
|
|
322
|
+
text += f"\n- [{m['alias']}] {truncate(m['subject'], 50)}{status_str}"
|
|
323
|
+
text += f"\n From: {m['sender']}"
|
|
324
|
+
text += f"\n Date: {m['date']}"
|
|
325
|
+
text += f"\n Location: {m['mailbox']} [{m['account']}]"
|
|
326
|
+
format_output(args, text, json_data=messages)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
# Registration
|
|
331
|
+
# ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
def register(subparsers) -> None:
|
|
334
|
+
"""Register message listing and reading subcommands."""
|
|
335
|
+
# list
|
|
336
|
+
p = subparsers.add_parser("list", help="List messages in a mailbox")
|
|
337
|
+
p.add_argument("-m", "--mailbox", default=None, help="Mailbox name (default: INBOX)")
|
|
338
|
+
p.add_argument("-a", "--account", help="Mail account name")
|
|
339
|
+
p.add_argument("--unread", action="store_true", help="Only show unread messages")
|
|
340
|
+
p.add_argument("--limit", type=int, default=DEFAULT_MESSAGE_LIMIT, help="Max messages to show")
|
|
341
|
+
p.add_argument("--after", help="Filter messages after date (YYYY-MM-DD)")
|
|
342
|
+
p.add_argument("--before", help="Filter messages before date (YYYY-MM-DD)")
|
|
343
|
+
p.add_argument("--json", action="store_true", help="Output as JSON")
|
|
344
|
+
p.set_defaults(func=cmd_list)
|
|
345
|
+
|
|
346
|
+
# read
|
|
347
|
+
p = subparsers.add_parser("read", help="Read full message details")
|
|
348
|
+
p.add_argument("id", type=int, help="Message ID")
|
|
349
|
+
p.add_argument("-a", "--account", help="Mail account name")
|
|
350
|
+
p.add_argument("-m", "--mailbox", help="Mailbox name (default: INBOX)")
|
|
351
|
+
p.add_argument("--short", action="store_true", help="Truncate body to 500 chars")
|
|
352
|
+
p.add_argument("--json", action="store_true", help="Output as JSON")
|
|
353
|
+
p.set_defaults(func=cmd_read)
|
|
354
|
+
|
|
355
|
+
# search
|
|
356
|
+
p = subparsers.add_parser("search", help="Search messages by subject or sender")
|
|
357
|
+
p.add_argument("query", help="Search term")
|
|
358
|
+
p.add_argument("--sender", action="store_true", help="Search in sender instead of subject")
|
|
359
|
+
p.add_argument("-a", "--account", help="Limit to specific account")
|
|
360
|
+
p.add_argument("-m", "--mailbox", help="Limit to specific mailbox (requires -a)")
|
|
361
|
+
p.add_argument("--limit", type=int, default=DEFAULT_MESSAGE_LIMIT, help="Max results")
|
|
362
|
+
p.add_argument("--json", action="store_true", help="Output as JSON")
|
|
363
|
+
p.set_defaults(func=cmd_search)
|