22"""
33jmapc-cli: Read-only JMAP CLI (JSON in/out)
44
5- Commands: session.get, email.query, email.get, mailbox.query, thread.get, pipeline.run
5+ Commands: session.get, email.query, email.get, email.read, mailbox.query, thread.get, pipeline.run
66Exit codes: 0 ok, 2 validation, 3 auth, 4 http, 5 jmap method error, 6 runtime.
77"""
88
@@ -244,7 +244,8 @@ def handle_email_get(args: argparse.Namespace) -> Tuple[int, Dict[str, Any]]:
244244 account_id = resolve_account_id (session_json , args .account )
245245 ids = parse_json_arg (args .ids )
246246 props = parse_json_arg (args .properties ) if args .properties else None
247- call = EmailGet (ids = ids , properties = props )
247+ fetch_all_body_values = getattr (args , "fetch_all_body_values" , False )
248+ call = EmailGet (ids = ids , properties = props , fetch_all_body_values = fetch_all_body_values )
248249 using , mrs = jmap_request (client , account_id , call , raise_errors = True )
249250 resp = mrs [0 ].response # EmailGetResponse
250251 capabilities_server = session_json .get ("capabilities" , {}).keys ()
@@ -269,6 +270,63 @@ def handle_email_get(args: argparse.Namespace) -> Tuple[int, Dict[str, Any]]:
269270 return 6 , envelope (False , "email.get" , vars (args ), meta_block (args .host , "unknown" , []), error = err )
270271
271272
273+ def handle_email_read (args : argparse .Namespace ) -> Tuple [int , Dict [str , Any ]]:
274+ try :
275+ client , session_json = build_client (args .host , args .api_token , args .timeout , not args .insecure )
276+ account_id = resolve_account_id (session_json , args .account )
277+
278+ # Request specific properties for reading
279+ props = ["subject" , "from" , "to" , "cc" , "bcc" , "textBody" , "bodyValues" , "receivedAt" , "messageId" ]
280+ call = EmailGet (ids = [args .id ], properties = props , fetch_all_body_values = True )
281+ using , mrs = jmap_request (client , account_id , call , raise_errors = True )
282+ resp = mrs [0 ].response
283+
284+ if not resp .data :
285+ raise ValueError (f"Email not found: { args .id } " )
286+
287+ email = resp .data [0 ]
288+
289+ # Extract plain text body
290+ body_text = ""
291+ if email .text_body and email .body_values :
292+ for part in email .text_body :
293+ part_id = getattr (part , "part_id" , None ) or (part .get ("partId" ) if isinstance (part , dict ) else None )
294+ if part_id and part_id in email .body_values :
295+ val = email .body_values [part_id ]
296+ content = getattr (val , "value" , None )
297+ if content :
298+ body_text += content
299+
300+ data = {
301+ "id" : getattr (email , "id" , args .id ),
302+ "subject" : email .subject ,
303+ "from" : [a .to_dict () if hasattr (a , "to_dict" ) else a for a in email .mail_from ] if email .mail_from else None ,
304+ "to" : [a .to_dict () if hasattr (a , "to_dict" ) else a for a in email .to ] if email .to else None ,
305+ "body" : body_text .strip (),
306+ }
307+
308+ capabilities_server = session_json .get ("capabilities" , {}).keys ()
309+ meta = meta_block (args .host , account_id , using , capabilities_server )
310+ return 0 , envelope (True , "email.read" , vars (args ), meta , data = data )
311+ except ValueError as exc :
312+ err = {"type" : "validationError" , "message" : str (exc ), "details" : {}}
313+ return 2 , envelope (False , "email.read" , vars (args ), meta_block (args .host , "unknown" , []), error = err )
314+ except ClientError as exc :
315+ err = {
316+ "type" : "jmapError" ,
317+ "message" : str (exc ),
318+ "details" : {"responses" : client_error_details (exc )},
319+ }
320+ return 5 , envelope (False , "email.read" , vars (args ), meta_block (args .host , "unknown" , []), error = err )
321+ except requests .HTTPError as exc :
322+ code = http_exit_code (exc .response .status_code )
323+ err = {"type" : "httpError" , "message" : str (exc ), "details" : {"status" : exc .response .status_code }}
324+ return code , envelope (False , "email.read" , vars (args ), meta_block (args .host , "unknown" , []), error = err )
325+ except Exception as exc :
326+ err = {"type" : "runtimeError" , "message" : str (exc ), "details" : {}}
327+ return 6 , envelope (False , "email.read" , vars (args ), meta_block (args .host , "unknown" , []), error = err )
328+
329+
272330def find_drafts_mailbox_id (client : Client , account_id : str ) -> str :
273331 """Find the Drafts mailbox ID by querying for role=drafts."""
274332 call = MailboxQuery (filter = {"role" : "drafts" })
@@ -817,6 +875,13 @@ def COMMAND_SPECS() -> Dict[str, Dict[str, Any]]:
817875 "options" : [
818876 {"name" : "ids" , "type" : "json" , "required" : True },
819877 {"name" : "properties" , "type" : "json" , "required" : False },
878+ {"name" : "fetch_all_body_values" , "type" : "flag" , "required" : False },
879+ ],
880+ },
881+ "email.read" : {
882+ "summary" : "Fetch email and return subject + plain text body" ,
883+ "options" : [
884+ {"name" : "id" , "type" : "string" , "required" : True },
820885 ],
821886 },
822887 "email.draft" : {
@@ -939,6 +1004,12 @@ def build_parser() -> argparse.ArgumentParser:
9391004 eg .set_defaults (json = "compact" )
9401005 eg .add_argument ("--ids" , required = True , help = "JSON array of ids or @file/@-" )
9411006 eg .add_argument ("--properties" , help = "JSON array of properties" )
1007+ eg .add_argument ("--fetch-all-body-values" , action = "store_true" , help = "Fetch all body values" )
1008+
1009+ erd = sub .add_parser ("email.read" , help = "Fetch email and return subject + plain text body" )
1010+ add_connection_opts (erd )
1011+ erd .set_defaults (json = "compact" )
1012+ erd .add_argument ("--id" , required = True , help = "Email ID" )
9421013
9431014 ed = sub .add_parser ("email.draft" , help = "Create draft email (does not send)" )
9441015 add_connection_opts (ed )
@@ -1010,6 +1081,7 @@ def build_parser() -> argparse.ArgumentParser:
10101081
10111082 pl = sub .add_parser ("pipeline.run" , help = "Run raw multi-call pipeline" )
10121083 add_connection_opts (pl )
1084+ pl .set_defaults (json = "compact" )
10131085 pl .add_argument ("--input" , required = True , help = "Pipeline JSON (inline/@file/@-)" )
10141086 pl .add_argument ("--allow-unsafe" , action = "store_true" , help = "Bypass read-only allowlist" )
10151087
@@ -1026,6 +1098,7 @@ def main(argv: Optional[List[str]] = None) -> int:
10261098 "session.get" : handle_session_get ,
10271099 "email.query" : handle_email_query ,
10281100 "email.get" : handle_email_get ,
1101+ "email.read" : handle_email_read ,
10291102 "email.draft" : handle_email_draft ,
10301103 "email.draft-reply" : handle_email_draft_reply ,
10311104 "email.changes" : handle_email_changes ,
0 commit comments