
    GVjI                        d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	m
Z
 ddlmZ ddlmZmZ ddZddZdd	Zdd
ZddZ	 d	 	 	 d dZd Zd!dZd"dZdZd#dZd$dZddZd%dZd&dZd'dZd(dZ d)d*dZ!	 d+	 	 	 d,dZ"d-d.dZ#d Z$y)/u  Email (send/read) in-process MCP for Jarvis — Namecheap Private Email.

Gives Claude a real mailbox: send mail, search the inbox, and read a message —
straight over SMTP+IMAP from Ahmed's own mailbox (``EMAIL_ADDRESS``). No API,
no OAuth, no third party: sending via SMTP from your own account is ordinary
email, so there's nothing to get "banned" for at personal/team volume.

Runs IN-PROCESS via the Claude Agent SDK's in-process MCP server (key
``"email"``; Claude sees ``mcp__email__<name>``). Pure standard library
(``smtplib`` / ``imaplib`` / ``email``) — no extra dependencies.

Config (``.env``):
  EMAIL_ADDRESS   the mailbox, also the From: address
  EMAIL_PASSWORD  its password (or a dedicated app password)
  EMAIL_SMTP_HOST / EMAIL_SMTP_PORT   default smtp.privateemail.com : 465 (SSL)
  EMAIL_IMAP_HOST / EMAIL_IMAP_PORT   default mail.privateemail.com  : 993 (SSL)
Set ``MCP_EMAIL=0`` to disable.
    )annotationsN)decode_headermake_header)EmailMessage)getaddressesparsedate_to_datetimec                 ^    t         j                  j                  dd      j                         S )NEMAIL_ADDRESS )osenvirongetstrip     A/Users/ahmed/devFolder/Ultron/claude-voice/voice/email_control.py_addrr       s     ::>>/2.4466r   c                     t         j                  j                  dd      dk(  ryt        t	               xr t         j                  j                  d            S )z2Whether the email MCP should be exposed to Claude.	MCP_EMAIL10FEMAIL_PASSWORD)r   r   r   boolr   r   r   r   enabledr   $   s<    	zz~~k3'3.<BJJNN+;<==r   c                    dd| dgiS )z2Wrap a plain string as an MCP text content result.contenttext)typer   r   )msgs    r   _textr    +   s    5677r   c                x    | y	 t        t        t        |                   S # t        $ r t        |       cY S w xY w)z?Decode an RFC2047 MIME header (subjects/senders) to plain text.r   )strr   r   	Exception)raws    r   _decoder%   0   s<    
{;}S1233 3xs   " 99c                   | j                         r}| j                         D ]i  }|j                         dk(  sdt        |j	                  d            vs4	 |j                  d      j                  |j                         xs dd      c S  y	 | j                  d      j                  | j                         xs dd      S # t        $ r Y w xY w# t        $ r t        | j                               cY S w xY w)	z9Extract a readable text body from a parsed email message.
text/plain
attachmentContent-DispositionTdecodeutf-8replacez(no plain-text body))	is_multipartwalkget_content_typer"   r   get_payloadr+   get_content_charsetr#   )r   parts     r   _body_ofr4   :   s    
HHJD%%'<7$C9N0O,PP++4+8??002=gyJ J	  &&d+22##%0)= 	=	 !   &3??$%%&s$   3C3C 	CC"C76C7c                    t         j                  j                  dd      }t        t         j                  j                  dd            }t	               }t               |d<   | |d<   |r||d<   ||d<   |j                  |       g }g }	|xs g D ]  }
t         j                  j                  t        |
      j                               }|s<t         j                  j                  |j                  d	            xs |}	 t        |d
      5 }|j                         }ddd       |j#                  dd|       |j!                  |        | dz   |z   j%                  d      D cg c]#  }|j                         s|j                         % }}t'        j(                  ||d      5 }|j+                  t               t         j                  d          |j-                  ||       ddd       d| d|  |rd| dndz   }|r#|dt/        |       ddj1                  |       z  }|	r|ddj1                  |	       z  }|dz   S # 1 sw Y   *xY w# t        $ r |	j!                  |       Y w xY wc c}w # 1 sw Y   xY w)u  Blocking SMTP send — called via asyncio.to_thread.

    `attachments` is an optional list of file paths. Each readable file is
    added as an application/octet-stream MIME part with
    ``Content-Disposition: attachment; filename=...`` (EmailMessage upgrades
    itself to multipart/mixed on the first add_attachment). Missing or
    unreadable files are SKIPPED and noted in the summary — never fatal.
    With no attachments the message is built exactly as before.
    EMAIL_SMTP_HOSTsmtp.privateemail.comEMAIL_SMTP_PORT465FromToCcSubject/rbNapplicationzoctet-stream)maintypesubtypefilename,   timeoutr   to_addrszSent 'z' to  (cc )r   z with z attachment(s): , u0    — couldn't attach (file missing/unreadable): .)r   r   r   intr   r   set_contentpath
expanduserr"   r   basenamerstripopenreadOSErrorappendadd_attachmentsplitsmtplibSMTP_SSLloginsend_messagelenjoin)tosubjectbodyccattachmentshostportr   attachedskippedprP   namefhdataarcptssouts                      r   _sendrq   M   sC    ::>>+-DEDrzz~~/78D
.C'CKCI	D	C	NOODHGBww!!#a&,,.1wwC 019T	dD!Rwwy "
 	4-#1D 	 	B  "$cB 5 5c :H :1aggiQWWY :EH			$b	1Q	$456	sU+ 
2 7)5
%"5A"
ECH&6tyy7J6KLLA$))GBTAUVV9# "! 	NN4 	 I	1	1sB   <III"I/8I/$?I4I	II,+I,4I=c                    t         j                  j                  dd      } t        t         j                  j                  dd            }t	        j
                  | |      }|j                  t               t         j                  d          |S )z/Open and log into IMAP (SSL). Caller closes it.EMAIL_IMAP_HOSTzmail.privateemail.comEMAIL_IMAP_PORT993r   )r   r   r   rN   imaplib	IMAP4_SSLr\   r   )re   rf   ms      r   _imapry   }   sc    ::>>+-DEDrzz~~/78D$%AGGEGRZZ 012Hr   c                   t               }	 |j                  d       | j                         r#|j                  ddddd|  ddd|  d      \  }}n|j                  ddd      \  }}|r|d	   r|d	   j	                         ng }|| d ddd
   }|s	 	 |j                          yg }|D ]  }|j                  d|d      \  }}|r |d	   rt        j                  |d	   d         nd}	|	rt        |	j                  d            nd}
|	rt        |	j                  d            nd}|	r|	j                  d      xs dnd}|j                  d|j                          d| d|
 d|         dj                  |      	 |j                          S # t        $ r Y yw xY w# t        $ r Y S w xY w# 	 |j                          w # t        $ r Y w w xY wxY w)u   Blocking IMAP search over INBOX — called via asyncio.to_thread.

    `query` is a plain phrase matched against sender+subject; empty = latest.
    Returns a numbered list (newest first) with the UID for email_read.
    INBOXsearchNORFROM"SUBJECTALLr   zNo matching messages.fetchz.(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])   r:   ?r=   Dater   z[uid z] z	
  from: z	
  subj: 
)ry   selectr   uidrY   logoutr#   emailmessage_from_bytesr%   r   rW   r+   r_   )querylimitrx   typrl   uidslinesr   hdhdrfrmsubdates                r   _searchr      s    	A	;;=hd$%lI5'|MIC he4IC"&47tAw}}UFG}TrT"*	HHJ CeeGSLNGC8:r!u%**2a584$C.1'#''&/*sC14'#''),-#C.1CGGFO)rrDLL5bj F$$'5* +  yy	HHJ 		y 			HHJ 		s[   BF& F #CF& 7F	FF	F#"F#&G(F98G9	GGGGc                \   t               }	 |j                  d       |j                  d| j                         d      \  }}|r|d   sd|  d	 |j	                          S t        j                  |d   d         }t        |j                  d            }t        |j                  d	            }t        |      }t        |      d
kD  r|dd
 dz   }d| d| d| 	 |j	                          S # t
        $ r Y S w xY w# t
        $ r Y S w xY w# 	 |j	                          w # t
        $ r Y w w xY wxY w)uI   Blocking IMAP fetch of one message body — called via asyncio.to_thread.r{   r   (RFC822)r   zNo message with uid rM   r   r:   r=   i  Nu   
…(truncated)zFrom: z

Subject: z

)ry   r   r   encoder   r#   r   r   r%   r   r4   r^   )r   rx   r   rl   r   r   r   rb   s           r   _readr      s$   A	EE'3::<<	T47)#a0	HHJ &&tAwqz2cggfo&cggi()}t9t;!33DuKuD7	HHJ 		y 			HHJ 		s[   AD	 C+A:D	 C:+	C76C7:	DD	D+DD+	D(%D+'D((D+)Sentz
INBOX.Sentz
Sent ItemszSent Messagesc                L    d| v r| j                  dd      \  }}|xs d|fS d| fS )z:'<folder>|<uid>' -> (folder, uid). A bare uid means INBOX.|r   r{   )rY   )identfolderr   s      r   _split_identr      s7    
e|kk#q)!'C''E>r   c                    | j                  |      \  }}|dk7  ry| j                  d|j                         d      \  }}|dk7  s|r|d   syt        j                  |d   d         S )z@Select `folder` and fetch one message by UID. None if it's gone.OKNr   r   r   r   )r   r   r   r   r   )rx   r   r   r   _rl   s         r   _fetchr      se    XXfFC
d{gszz|Z8IC
d{$d1g##DGAJ//r   c                (   | j                  d      xs dj                         }|r|d   j                         S | j                  d      xs dj                         j                         }|r|d   S | j                  d      xs dj                         S )z{The thread's root Message-ID: first of References, else In-Reply-To,
    else the message's own id (it started the thread).
Referencesr   r   In-Reply-To
Message-ID)r   rY   r   )r   refsirts      r   _root_idr      s     GGL!'R..0DAw}}77=!'R
.
.
0
6
6
8C
1vGGL!'R..00r   c                (   | j                  |      \  }}|dk7  rg S 	 | j                  dddddd| dddd| d	      \  }}|dk7  s|r|d	   sg S |d	   j                         D cg c]  }|j	                          c}S # t        $ r g cY S w xY wc c}w )
zDUIDs in `folder` whose References or Message-ID mention the root id.r   r|   Nr}   HEADER
REFERENCESr   z
MESSAGE-IDr   )r   r   r#   rY   r+   )rx   r   rootr   r   rl   us          r   _thread_uidsr      s    XXfFC
d{	EE(D$"LAdV1+"LAdV1+?	T
 d{$d1g	 $Q01AHHJ00	  	 1s   $A> $B>BBc           	     J   d\  }}i }g }t        | j                               D ]g  \  }}|j                         }|j                  d      r)t	        |j                  d      xs d      j                         }t        |j                               }	|j                  d      xs dj                         j                  d      }
	 |j                  d      xs d	}|
r;|j                  d
      r*|t        j                  |      j                         d||
<   |	sd|v r2|j                  t	        |      |	xs d| |t!        |      dd       ,|j                  |j#                         xs dd      }|dk(  r||z  }\|dk(  sc||z  }j ||||dS # t        $ r Y ~w xY w)u  Split a parsed message into text body, HTML body, inline (cid:) images
    and real attachments. Untruncated — the card shows everything.

    `att_id` is the part's index in walk order: stable for a given message, and
    all IMAP gives us (there are no Gmail-style attachment ids).)r   r   z
multipart/r)   r   z
Content-IDz<>Tr*   r   zimage/)mimeb64r(   part-)att_idrC   r   sizerP   r,   r-   r'   z	text/html)	body_text	body_htmlinlinerd   )	enumerater/   r0   
startswithr"   r   lowerr%   get_filenamer   r1   r#   base64	b64encoder+   rW   r^   r2   )r   r   htmlr   attsidxr3   ctypedispfnamecidr$   rb   s                r   	_parts_ofr     s    JD$ FDsxxz*	T%%'L)488128b9??A))+,xx%+224::4@	""$"/63C 5##H-#("("2"23"7">">"@BF3KLD(KK3s8%*%;cUm!&C"F G zz$224?KL DLDk!DLD1 +2 DF! !!  		s   F	F"!F"c           
        	 t        |j                  d            j                         }t	        |j                  d            }|  d| |t	        |j                  d            t	        |j                  d            t	        |j                  d            |j                  d      xs d|t               j                         |j                         v d	t        |      S # t        $ r d}Y w xY w)
z2One parsed message in the shape mail_view expects.r   g        r:   r   r;   r<   r=   r   )msg_idfromr`   rc   ra   r   tsoutgoing)r   r   	timestampr#   r%   r   r   r   )r   r   r   r   r   s        r   _as_msgr   (  s    "3776?3==? #''&/
"CHAcU#73774=19O3779-.8M2GMMOsyy{2 C.   s   (C   C.-C.c           	        t        |       \  }}t               }	 t        |||      }|t        d|        t	        |||      g}| d| h}t        |      }|rjdgt        D ]]  }t        |||      D ]K  }	| d|	 }
|
|v r|j                  |
       t        |||	      }|0|j                  t	        ||	|             M _ |j                  d        t        d |D        d      }|d   d	   t               ||d
	 |j                          S # t        $ r Y S w xY w# 	 |j                          w # t        $ r Y w w xY wxY w)zA best-effort THREAD around one message, for the HUD card. Falls back to
    the single message when the server can't be searched by header.no message r   r{   c                    | d   S )Nr   r   )xs    r   <lambda>zthread.<locals>.<lambda>P  s    qwr   )keyc              3  2   K   | ]  }|d    s	|d      yw)ra   Nr   ).0r   s     r   	<genexpr>zthread.<locals>.<genexpr>Q  s     BcQy\)cs   

z(no subject)r   r   )	thread_idaccount_addrra   messages)r   ry   r   
ValueErrorr   r   _SENT_FOLDERSr   addrW   sortnextr   r   r#   )r   r   r   rx   r   rp   seenr   foldu2r   otherra   s                r   threadr   :  se    u%KFCAQ$;{5'233vsC()(!C5!"} 1=1&q$5B!F!B4.Cd{ HHSM"1dB/E(

74U#;< 6 2 	&'BcB%' WX."5	HHJ 			HHJ 		sI   B	D $AD ?D	DDE!D21E2	D>;E=D>>Ec                   t               }	 |j                  d       | j                         r#|j                  ddddd|  ddd|  d      \  }}n|j                  ddd      \  }}|d	k(  r|r|d
   r|d
   j	                         ng }|| d ddd   D cg c]  }d|j                           c}	 |j                          S c c}w # t        $ r Y S w xY w# 	 |j                          w # t        $ r Y w w xY wxY w)zv'<folder>|<uid>' ids for messages matching a phrase (sender/subject),
    newest first. Empty query = the latest mail.r{   r|   Nr}   r~   r   r   r   r   r   r   zINBOX|)ry   r   r   r   rY   r+   r   r#   )r   r   rx   r   rl   r   r   s          r   search_uidsr   \  s    	A	;;=hd$%lI5'|MIC he4IC"%+$47tAw}}/3UFG}TrT/BC/B!&%/BC	HHJ D  			HHJ 		sT   B	C C/C 2CC 	CCC9C*)C9*	C63C95C66C9c                d   ddl m} t        |       \  }}t               }	 t	        |||      }|t        d|        t        |      }	t        |j                               D ]  \  }
}|
|	k7  r|j                  d      xs d}|xs  t        |j                               xs d|
 }|j                  |xs | |      }|j                  |       t        |      c 	 |j                          S  t        d| d	|        # t         $ r Y S w xY w# 	 |j                          w # t         $ r Y w w xY wxY w)
zmDownload one attachment (by its walk index) to
    ~/.jarvis/attachments/<thread>/ and return the local path.r   )	mail_viewr   Tr*   r   r   zno attachment z on )voicer   r   ry   r   r   rN   r   r/   r1   r%   r   	dest_pathwrite_bytesr"   r   r#   )r   r   rC   r   r   r   r   rx   r   wantr   r3   r$   rj   rP   s                  r   r(   r(   p  s8     u%KFCAQ$;{5'2336{"388:.ICd{""$"/63CLwt'8'8':;Lse}D&&y'9E4@DS!t9	HHJ / >&eW=>>  			HHJ 		sH   B:D C>,D >	D
	D
D/D D/ 	D,)D/+D,,D/c                   t        |       \  }}t               }	 t        |||      }	 |j                          |t        d|        t        |j                  d            }t        |j                  d            }|j                         j                  d      sd|z   }|j                  d      xs dj                         }	|j                  d	      xs dd
z   |	z   j                         }
d}|rt               j                         ht        |g      D ch c]  \  }}|s	|j                          c}}z  }g }t        t        |j                  d            t        |j                  d            g      D ]K  \  }}|s	|j                         |vs|j                  |j                                |j                  |       M dj                  |      }t         j"                  j                  dd      }t%        t         j"                  j                  dd            }t'               }t               |d<   ||d<   |r||d<   ||d<   |	r
|	|d<   |
|d	<   |j)                  |       t        ||g      D cg c]
  \  }}|s	| }}}t+        j,                  ||d      5 }|j/                  t               t         j"                  d          |j1                  ||       ddd       d| |rd| dz   dz   S dz   dz   S # t        $ r Y w xY w# 	 |j                          w # t        $ r Y w w xY wxY wc c}}w c c}}w # 1 sw Y   bxY w)zReply in-thread over SMTP, setting In-Reply-To + References so the
    correspondent's client threads it. reply_all keeps the other recipients.Nr   r:   r=   zre:zRe: r   r   r    r;   r<   rL   r6   r7   r8   r9   r   rE   rF   r   rH   zReplied to rJ   rK   rM   )r   ry   r   r   r#   r   r%   r   r   r   r   r   r   r   rW   r_   r   r   rN   r   rO   rZ   r[   r\   r]   )r   rb   	reply_allr   r   rx   origr`   subjr   r   rc   _nrm   r   keepre   rf   r   rn   ro   s                        r   replyr     s    u%KFCAa%	HHJ |;ug.//	&!	"B488I&'D::<""5)}hh|$*113FXXl#)rS069@@BD	B <;M#S;M%"aQRAGGI;M#SS!7488D>#:#*488D>#:#< =EBQWWYd*#A	=
 YYt_::>>+-DEDrzz~~/78D
.C'CKCI	D	C	N#M LOOD("b28252qaQ2E8			$b	1Q	$456	sU+ 
2 "5A=CC"=CCI  			HHJ 		 $T( 9	1	1sd   L K? 
L4"L46
L:L: ?M ?	LLL1L"!L1"	L.+L1-L..L1 M	c            	         ddl ddlm} m}  | ddt        t        t        t        d      dfd       } | dd	t        t
        d
      dfd       } | dddt        i      dfd       } |dd|||g      S )z5Build and return the in-process ``email`` MCP server.r   N)toolcreate_sdk_mcp_server
email_sendzbSend an email from Ahmed's mailbox. `to` and `cc` are comma-separated addresses; `cc` is optional.)r`   ra   rb   rc   c                  K   t        | j                  dd            j                         }|st        d      S 	 j	                  t
        |t        | j                  dd            t        | j                  dd            t        | j                  dd            j                                d {   }t        |      S 7 # t        $ r}t        d|       cY d }~S d }~ww xY ww)Nr`   r   z email_send failed: no recipient.ra   rb   rc   zemail_send failed: )r"   r   r   r    	to_threadrq   r#   )argsr`   rp   easyncios       r   r   z build_server.<locals>.email_send  s     
 $#$**,;<<	4))r3txx	267DHHVR()3txxb/A+B+H+H+JL LC :L  	4.qc233	4sG   7C&A5C 0C 1C ?C& C 	C#CC#C&C##C&email_searchzSearch the inbox by a phrase matched against sender and subject (empty = latest messages). Returns a list with a UID for each; pass a UID to email_read to read the full message.)r   r   c           
     D  K   	 t        dt        t        | j                  d      xs d      d            }j	                  t
        t        | j                  dd            |       d {   }t        |      S 7 # t        $ r}t        d|       cY d }~S d }~ww xY ww)Nr   r   
      r   r   zemail_search failed: )	maxminrN   r   r   r   r"   r    r#   )r   r   rp   r   r   s       r   r   z"build_server.<locals>.email_search  s     	63s488G#4#:;R@AE))TXXgr23U< <C:<  	60455	6sG   B A%A< *A:+A< 9B :A< <	BBBB BB 
email_readzGRead the full body of one inbox message by its UID (from email_search).r   c                  K   t        | j                  dd            j                         }|st        d      S 	 j	                  t
        |       d {   }t        |      S 7 # t        $ r}t        d|       cY d }~S d }~ww xY ww)Nr   r   zemail_read failed: no uid.zemail_read failed: )r"   r   r   r    r   r   r#   )r   r   rp   r   r   s       r   r  z build_server.<locals>.email_read  s      $((5"%&,,.566	4))%55C: 6 	4.qc233	4sF   7B
A& A$A& #B
$A& &	B/B<B=B
BB
r   z1.0.0)rj   versiontools)r   dictreturnr
  )r   claude_agent_sdkr   r   r"   rN   )r   r   r   r   r  r   s        @r   build_serverr    s    <	,9c
=?
4	?
4 
.: #
&	(
6(
6 
,"CL*4*4 !<4 r   )r  r"   )r  r   )r   r"   r  r
  )N)r`   r"   ra   r"   rb   r"   rc   r"   rd   zlist[str] | Noner  r"   )r   r"   r   rN   r  r"   )r   r"   r  r"   )r   r"   r  ztuple[str, str])r   r"   r   r"   )r   r"   r   r"   r  	list[str])r  r
  )r   r"   r   r"   r  r
  )r   r"   r  r
  )   )r   r"   r   rN   r  r  )r   )
r   r"   r   r"   rC   r"   r   r"   r  r"   )F)r   r"   rb   r"   r   r   r  r"   )%__doc__
__future__r   r   r   rv   r   rZ   email.headerr   r   email.messager   email.utilsr   r   r   r   r    r%   r4   rq   ry   r   r   r   r   r   r   r   r   r   r   r   r(   r   r  r   r   r   <module>r     s   & #    	  3 & ;7>8
&( +/-'-36-`!HL F0	11 #!L$D* !#'*:.Db5r   