
    HVjV                       U 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mZmZm	Z	 ddl
mZ ddlmZ ddlmZ ddlmZ dd	lmZ  e	 ed
            Z ej*                         dz  dz  Zi Zded<   g Zded<   dZd9dZh dZh dZh dZh dZh dZ  ejB                  dejD                        Z# ejB                  dejD                        Z$ ejB                  dejD                        Z% ejB                  d      Z& G d de      Z'	 	 d:	 	 	 d;dZ(d<d Z)d=d!Z*d>d"Z+d?d#Z,d@d$Z-dAd%Z.dBd&Z/dCd'Z0dDd(Z1dDd)Z2dEd*Z3dEd+Z4dFd,Z5dGdHd-Z6dGdId.Z7dJd/Z8dGdKd0Z9dGdKd1Z:dLd2Z;	 dG	 	 	 dMd3Z<dNd4Z=dOd5Z>	 	 dP	 	 	 dQd6Z?dRd7Z@d8 ZAy)SuK  Email reader CARD — the whole thread on Ahmed's HUD, never in a browser.

Ahmed has a standing NEVER rule against Gmail-in-the-browser (enforced in code
by voice/show_gate.py). This card is the SANCTIONED way for him to actually
READ mail: he says "pull up Elie's email", Jarvis calls ``mail_show``, the
engine fetches the FULL thread (every reply, the HTML body, the attachments),
sanitises it, and emits ONE event — the HUD opens a reader card he can scroll,
open attachments from, and REPLY from. There is no browser anywhere in here,
and there must never be.

  Jarvis (voice) -> mail_show / mail_show_id  (open the card)
                    mail_close                (drop it)
  Ahmed  (HUD)   -> clicks an attachment chip / Load images / Send
                    -> HUD writes control/email_action.json
                    -> ControlWatcher._email_action applies it and, for a sent
                       reply, drops a note in the master's inbox so Jarvis says
                       it out loud.

Two mailboxes, two backends, one card:
  work      Gmail API (voice/gmail_control.py) — real thread ids
  business  ahmad@revalstudio.com over IMAP (voice/email_control.py) — the
            "thread" is reassembled from References/In-Reply-To, best effort

Engine → HUD events (voice.events.emit):
  email_thread      {thread_id, account, subject, messages[]}   (oldest first)
  email_attachment  {thread_id, msg_id, att_id, path, ok, error}
  email_reply_sent  {thread_id, ok, error}
  card_close        {draft_id: thread_id}    (reuses the compose card's close)
Set MCP_MAIL_CARD=0 to disable.
    )annotationsN)datetime	timedeltatimezone)	parseaddr)escape)
HTMLParser)Path)emit   )hoursz.jarvisattachmentszdict[str, dict]_THREADSz	list[str]_ORDERzNdata:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7c                     t         j                  j                  dd      dk(  ryddlm} m} |j                         xs | j                         S )NMCP_MAIL_CARD10Fr   email_controlgmail_control)osenvirongetvoicer   r   enabledr   s     =/Users/ahmed/devFolder/Ultron/claude-voice/voice/mail_view.pyr   r   <   s<    	zz~~os+s22  "=m&;&;&==    >   svgformmathembedframeappletiframeobjectscriptframesetnoscripttemplate>   baselinkmeta>   brhrcolimgwbrarear+   r,   r-   r"   inputparamtracksource>	   srccitedatahrefactionpostercodebaselongdesc
background>   
http-equiv
xlink:hrefpingsrcset
formactionz%^(javascript|vbscript|data:text/html)z!url\(\s*['\"]?\s*https?://[^)]*\)z7(javascript|vbscript)\s*:|expression\s*\(|@import[^;]*;z<[^>]+>c                  d     e Zd ZdZd f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
Z xZS )
_Sanitizeru  Rebuilds the HTML, keeping only what's safe to render.

    Drops script/iframe/object/embed/svg/form subtrees, link/meta/base tags,
    every on*= handler and javascript: URL. Inline cid: images become data:
    URIs from the message's own parts (safe — they already arrived with the
    mail). Remote http(s) images become a transparent pixel until Ahmed hits
    "Load images" (they're tracking pixels far more often than they're
    pictures).c                    t         |   d       |xs i | _        || _        d| _        g | _        d| _        d| _        y )NT)convert_charrefsr   F)super__init__inlineblockblockedout_skip_css)selfrL   block_remote	__class__s      r   rK   z_Sanitizer.__init__b   sA    $/l!
 
	r   c                   | j                   r|t        v r| xj                   dz  c_         y |t        v rd| _         y |t        v ry |dk(  rd| _        | j                  j                  d| | j                  ||       d       y )N   styleT<>)rP   
_DROP_TREE	_DROP_TAGrQ   rO   append_attrsrR   tagattrss      r   handle_starttagz_Sanitizer.handle_starttagl   su    ::j 

a
*DJ)'>DI!C5S%!8 9;<r   c                    | j                   s|t        v s|t        v ry | j                  j	                  d| | j                  ||       d       y )NrX   z />)rP   rZ   r[   rO   r\   r]   r^   s      r   handle_startendtagz_Sanitizer.handle_startendtagz   sC    ::
*cY.>!C5S%!8 9=>r   c                    | j                   r|t        v r| xj                   dz  c_         y |t        v s|t        v ry |dk(  rd| _        | j
                  j                  d| d       y )NrV   rW   Fz</rY   )rP   rZ   r[   _VOIDrQ   rO   r\   )rR   r_   s     r   handle_endtagz_Sanitizer.handle_endtag   sX    ::j 

a
)se|'>DI"SE$r   c                    | j                   ry | j                  j                  | j                  r| j	                  |             y t        |d             y )NFquote)rP   rO   r\   rQ   cssr   rR   r:   s     r   handle_dataz_Sanitizer.handle_data   s6    ::$))SE9RSr   c                     y N rk   s     r   handle_commentz_Sanitizer.handle_comment   s    r   c           
        g }|D ]  \  }}|xs dj                         }|xs d}|j                  d      s|t        v r:|dk(  r| j                  |      }nE|t        v r| j                  |||      }|o|}n$t        j                  |j                               r|j                  d| dt        |d       d        dj                  |      S )	N onrW    z="Trh   ")lower
startswith_KILL_ATTRSrj   
_URL_ATTRSurl_JS_REmatchstripr\   r   join)	rR   r_   r`   rO   raw_kraw_vkvrz   s	            r   r]   z_Sanitizer._attrs   s    !LE5"##%AA||D!Q+%5G|HHQKjhhsAq);aggi(JJ1#Rq 56a89 " wws|r   c                ~   |xs dj                         }t        j                  dd|      j                         }|rt        j                  |      ry|j                  d      r\| j                  j                  |j                  dd      d   j                         j                  d            }|syd|d	    d
|d    S |j                  d      r|j                  d      r|S dS |j                  d      r0|dk(  r|S | j                  r| xj                  dz  c_        t        S |S |dk(  r|j                  d      r|S y)z>Rewrite one URL attribute. None = drop the attribute entirely.rr   z[\s\x00-\x1f]Nzcid::rV   z<>zdata:mimez;base64,b64zdata:image/)zhttp://zhttps://r;   )zmailto:ztel:)r}   resubrv   r{   r|   rw   rL   r   splitrM   rN   _PIXEL)rR   r_   keyvaluesflatparts          r   rz   z_Sanitizer.url   s   [b!vv&A.446v||D)??6";;??1773?1#5#;#;#=#C#CD#IJD4<.e>>??7#61@D@??23f}zz!H&=T__-@AHr   c                ~    t         j                  d|xs d      }| j                  rt        j                  d|      }|S )zNeutralise CSS: no javascript:/expression()/@import, and no remote
        url() (a background image tracks just as well as an <img>).rr   zurl(about:blank))
_CSS_JS_REr   rM   _CSS_URL_RE)rR   rj   rO   s      r   rj   z_Sanitizer.css   s5     nnR+:://"4c:C
r   )rL   dictrS   boolreturnNone)r_   strr`   listr   r   )r_   r   r   r   )r:   r   r   r   )r_   r   r`   r   r   r   )r_   r   r   r   r   r   r   
str | None)rj   r   r   r   )__name__
__module____qualname____doc__rK   ra   rc   rf   rl   rp   r]   rz   rj   __classcell__)rT   s   @r   rG   rG   X   s6    =?
	%T
&0r   rG   c                   | syt        |xs i |      }	 |j                  |        |j                          dj                  |j                        |j                  fS # t        $ r& t	        t
        j                  d|       d      dfcY S w xY w)z5Sanitised HTML + how many remote images were blocked.)rr   r   rt   Frh   r   rr   )
rG   feedclose	Exceptionr   _TAG_REr   r~   rO   rN   )htmlrL   rS   ps       r   sanitize_htmlr      s~     6<R.A>	t		 77155>199$$  >gkk#t,E:A==>s   !A ,BBc                    t        j                  dd| xs d      }t        j                  dd|      }t        j                  d|      }ddlm} t        j                  dd	 ||            j                         S )
z=Rough text fallback for a message that only has an HTML body.z"(?is)<(script|style)[^>]*>.*?</\1>rt   rr   z(?i)<br\s*/?>|</p>|</div>|</tr>
r   )unescapez\n{3,}z

)r   r   r   r   r   r}   )r   txtr   s      r   _plainr      s_    
&&6TZR
HC
&&3T3
?C
++b#
C66)VXc]399;;r   c                   t         j                  j                  | xs dj                  dd      j	                               }t        j                  dd|      j	                  d      }|j                  dd      }|dd	 xs d
S )zA filename that can only ever land INSIDE the attachment dir: no path
    separators, no '..', no control characters, no leading dot.rr   \/z[\x00-\x1f/]+z. z...Nx   
attachment)r   pathbasenamereplacer}   r   r   )namer+   s     r   	safe_namer      sq     77TZR00s;AACDD66"B-33D9D<<c"D:%%r   c                p    t         t        | xs d      z  }|j                  dd       |t        |      z  S )z>Where an attachment of this thread gets written (dir created).mailT)parentsexist_ok)ATT_ROOTr   mkdir)	thread_idfilenameds      r   	dest_pathr      s6    9Y0&11AGGD4G(y"""r   c                   | xs dj                         j                         }ddlm}m} d|v sd|v r|j                         ry|j                         s|j                         ry|r|j                  |      S |j                         S )zA spoken account word -> a backend account. 'business'/'reval' = the
    revalstudio IMAP mailbox; anything else = a Gmail label (default 'work').rr   r   r   businessreval)r}   rv   r   r   r   r   _resolve_default_label)accountar   r   s       r   _accountr      sx     
B%%'A2a7a<]-B-B-D  "}'<'<'>()=!!!$M}/K/K/MMr   c                   t         j                  j                  dd      j                  d      D ch c]0  }|j	                         r|j	                         j                         2 }}| t         j                  j                  dd      fD ]2  }|s|j                  |j	                         j                                4 |S c c}w )up   Every address that means 'Ahmed sent this' — the mailbox's own address
    plus anything in MAIL_MY_ADDRESSES.MAIL_MY_ADDRESSESrr   ,EMAIL_ADDRESS)r   r   r   r   r}   rv   add)account_addrr   mines      r   _my_addressesr      s     JJNN.399#>M> #$BC'') GGIOO> 	 MBJJNN?B?@HHQWWY__&' A KMs   5Cc                    ddl m} 	  ||       }|j                   |j                  t        j
                        }|S # t        $ r, t        j                  |xs dt        j
                        cY S w xY w)Nr   )parsedate_to_datetime)tzinfo)tz)	email.utilsr   r   r   r   utcr   r   fromtimestamp)raw_datetsr   r   s       r   _whenr     sd    1@!(+88			.A @%%bgA(,,??@s   5> 2A32A3c                 4    t        j                  t              S rn   )r   now_TZro   r   r   _nowr     s    <<r   c                `   t               }| j                  t              } | j                         |j                         k(  r| j	                  d      S || z
  j
                  dk  r| j	                  d      S | j                  |j                  k(  r| j	                  d      S | j	                  d      S )u5   Pre-formatted Riyadh time — the HUD just prints it.z%H:%M   z%a %H:%Mz%d %b %H:%Mz%d %b %Y)r   
astimezoner   datestrftimedaysyear)r   r   s     r   _humanr     s    
&C	SAvvx388:zz'""a~~zz*%%vvzz-((::j!!r   c                R   t         |    }|d   }t        |j                  dd            }g }|j                  dg       D ]  }|j                  dd      }t        |j                  dd            \  }}||d   v}	t	        |j                  dd      |j                  d	      xs i |	      \  }
}|j                  d
d      xs t        |j                  dd            }t        |j                  dd      |j                  dd            }g }|j                  dg       D ]R  }|j                  i |d|d   j                  ||j                  dd      f|j                  dd      xs d      i       T |j                  ||xs |xs d||j                  dd      |j                  dd      |j                  t              j                         t        |      t        |j                  d            xs |j                         |v |
|||d        | |d   |j                  dd      |dS )z?Build the `email_thread` event body from the cached raw thread.rawr   rr   messagesmsg_idfrom	unblocked	body_htmlrL   	body_textr   r   r   r   r   pathsatt_idz	(unknown)toccoutgoing)r   	from_name	from_addrr   r   r   
date_humanr   r   r   images_blockedr   r   subjectz(no subject)r   r   r   r   )r   r   r   r   r   r   r   r\   r   r   	isoformatr   r   rv   )r   str   r   msgsmmidr   addrrM   r   rN   textwhenattsr   s                   r   payloadr  (  s   	)	B
U)C45DDWWZ$eeHb!quuVR01
d2k?*%aeeK&<&'eeHo&;UDguu["%Gk20F)GQUU62&dA7}b)AKK F1 FfbkooaeeHb)*AEE&",=,C'E F G * 	44%%b/tROOC(224 ,QUU:./G4::<43GD%
 	 %0 #r)}wwy.9tM Mr   c                T    t        |       }t        d|d   |d   |d   |d          |S )Nemail_threadr   r   r   r   r   )r  r   )r   r   s     r   emit_threadr  J  s4    	A1[>1Y<9*7Hr   c                j    | dk(  rddl m} |j                  |      S ddl m} |j                  | |      S )z7Pull a thread from whichever backend owns this account.r   r   r   r   )r   r   threadr   )r   identr   r   s       r   _fetchr  Q  s4    *'##E**#//r   c                    t        | |      }|j                  d      xs |}| |t               i dt        |<   |t        vrt        j                  |       t        |      S )z[Fetch a thread, cache it, and put it on Ahmed's HUD. Returns the
    payload that went out.r   )r   r   r   r   )r  r   setr   r   r\   r  )r   r  r   tids       r   open_threadr  Z  sY     %
 C
''+

'%C '#% "HSM
&csr   c                *   | d   }t        |      }t        d |D              }t        d t        |      D        |r|d   d   nd      }| d|dk7  rd	nd
 g}|r|j	                  | d|dk7  rd	nd
        d| d    d| ddj                  |       dS )z0The one line Jarvis speaks after the card lands.r   c              3  8   K   | ]  }t        |d            yw)r   N)len.0r   s     r   	<genexpr>z_summary.<locals>.<genexpr>j  s     3ds1]#$ds   c              3  2   K   | ]  }|d    r	|d     yw)r   r   Nro   r  s     r   r  z_summary.<locals>.<genexpr>l  s     L1a
m+s   

r   ?z messagerV   r   rr   z attachmentzPulled up the 'r   z' thread with u    on Ahmed's HUD — z, z. He's reading it on the card.)r  sumnextreversedr\   r~   )r   r   nr  whobitss         r   _summaryr   f  s    Z=DD	A3d33D
LL(,tBx$#7Cca#R012DtfKtqyb'ABCa	l^># ?99T?##AC Dr   c                    t        |      }|dk(  rddlm} |j                  | d      }nddlm} |j                  || d      }|s	d|  d| dS t        t        ||d               S )	z=Newest thread matching `query` -> card. Returns Jarvis' line.r   r   r  rV   r	  zNo message matching 'z	' in the z	 mailbox.)r   r   r   search_uidsr   
search_idsr   r  )queryr   acctr   idsr   s         r   showr'  u  sn    GDz'''q1'&&tUA6&ugYtfIFFKc!f-..r   c                >    t        t        t        |      |             S )zACard for an explicit message/thread id (chains off gmail_search).)r   r  r   r   r   s     r   show_idr*    s    K 16:;;r   c                 $    t         r	t         d   S d S )Nr  )r   ro   r   r   	latest_idr,    s    6":)T)r   c                    | xs t               xs d}|syt        j                  |d       |t        v rt        j	                  |       yy)u   Forget the thread engine-side. Used when the HUD already closed the card
    itself (Ahmed clicked ✕) — no event, the card is gone already.rr   N)r,  r   popr   remover   r  s     r   dropr1    sA     
(y{
(bCLLd
f}c r   c                ^    | xs t               xs d}|syt        |       t        d|       y)un   Drop the thread AND tell the HUD to dismiss the card (Jarvis closing it
    on Ahmed's word — `mail_close`).rr   N
card_close)draft_id)r,  r1  r   r0  s     r   r   r     s,     
(y{
(bCI$r   c                P    t         j                  |       }|r|d   S t        |      S )Nr   )r   r   r   )r   r   r   s      r   _acct_ofr6    s&    	i	 B2i=5HW$55r   c                \   t        | |      }	 |dk(  rddlm} |j                  ||||       }nddlm} |j                  |||||       }t        j                  |       }
|
r
||
d   ||f<   t        d| |||dd
       |S # t
        $ r$}	t        d| ||ddt        |	      dd	 
        d}	~	ww xY w)z|Ahmed clicked an attachment chip: fetch it, remember the path (so a
    re-emit keeps it), and tell the HUD where it landed.r   r   r  r	  email_attachmentrr   FN   )r   r   r   r   okerrorr   T)
r6  r   r   r   r   r   r   r   r   r   )r   r   r   r   r   r%  r   r   r   er   s              r   downloadr=    s     Iw'D:+ ++FFHiPD+ ++D&&(,57D 
i	 B	(,7VV$%	yt2'K  9VSVDS\	Cs   ;A> >	B+B&&B+c                   t        | xs d      j                         }|rt        j                  j	                  |      sy	 t
        j                  dk(  rt        j                  d|g       |S t        j                  dk(  rt        j                  |       |S t        j                  d|g       	 |S # t        $ r%}t        dt        |      dd         Y d}~|S d}~ww xY w)	z@Open a downloaded attachment in its default app (Preview, etc.).rr   darwinopenntzxdg-openz  [mail] open failed: Nr   )r   r}   r   r   isfilesysplatform
subprocessPopenr   	startfiler   print)r   r   r<  s      r   	open_pathrI    s    DJBABGGNN1%7<<8#fa[) H WW_LLO
 H j!_- H  7&s1vds|n566H7s$   *B1 -(B1 B1 1	C:CCc                r    t         j                  |       }|sy|d   j                  |       t        |        y)u   Ahmed hit "Load images" on one message — re-emit the thread with that
    message's remote images restored (the rest stay blocked).Nr   )r   r   r   r  )r   r   r   s      r   load_imagesrK    s2     
i	 B{O	r   c           	     X   t        | |      }t        |      j                         sy	 |dk(  rddlm} |j                  |||      }nddlm} |j                  ||||      }t        d| dd	       d|fS # t        $ r,}	t        d| d
t        |	      dd 	       d
d|	 fcY d}	~	S d}	~	ww xY w)zSend Ahmed's typed reply on the thread. Threading headers are preserved
    by the backends (Gmail: threadId + In-Reply-To/References; IMAP: the same
    two headers). Returns (ok, human_summary).)Fzthe reply was empty.r   r   r  r	  email_reply_sentTrr   )r   r:  r;  FNr9  zfailed: )
r6  r   r}   r   r   replyr   _replyr   r   )
r   r   body	reply_allr   r%  r   rO   r   r<  s
             r   rN  rN    s    
 Iw'Dt9??,%:+%%fdI>C+&&tVT9EC9RHSy %9q6$3<	!n$$%s   AA4 4	B)=!B$B)$B)c                    dd| dgiS )Ncontentr   )typer   ro   )msgs    r   _textrV    s    5677r   c                     ddl ddlm} m}  | ddt        t        d      dfd       } | dd	t        t        d
      dfd       } | dddt        i      dd       } |dd|||g      S )z)Build the in-process ``mail`` MCP server.r   N)toolcreate_sdk_mcp_server	mail_showu  Put an email THREAD on Ahmed's HUD as a readable card — use this whenever he wants to SEE/read/'pull up' an email (never open Gmail in a browser, he has a hard rule against it). `query` matches the sender's name/address or subject words (Gmail search syntax works on the work account). `account` = work (default) or business (ahmad@revalstudio.com). Shows the whole thread — every reply, the formatted body, the attachments — and he can reply from the card.)r$  r   c           
     N  K   t        | j                  dd            j                         }	 j                  t        |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$  rr   r   zmail_show failed: )r   r   r}   	to_threadr'  rV  r   )argsqrO   r<  asyncios       r   rZ  zbuild_server.<locals>.mail_show  s      "%&,,.	3))aTXXi45;;=? ?C:?  	3-aS122	3sG   *B%AB /A?0B >B%?B 	B"
BB"B%B""B%mail_show_idu   Same as mail_show but by an explicit message/thread id — chain it off a gmail_search / email_search result when you already know which message he means.r)  c           
     h  K   t        | j                  dd            j                         }|st        d      S 	 j	                  t
        |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   rr   z#mail_show_id failed: no message id.r   zmail_show_id failed: )r   r   r}   rV  r\  r*  r   )r]  r   rO   r<  r_  s       r   r`  z"build_server.<locals>.mail_show_id  s      $((8R()//1>??	6))c$((9b"9:@@BD DC:D  	60455	6sG   7B2AB <B=B B2B 	B/B*$B/%B2*B//B2
mail_closezCClose the email card on Ahmed's HUD ('close that', 'done with it').r   c                   K   t        | j                  dd            j                         xs
 t               }|st	        d      S t        |       t	        d      S w)Nr   rr   zno email card is open.zClosed the email card.)r   r   r}   r,  rV  r   )r]  r  s     r   rb  z build_server.<locals>.mail_close!  sO      $((;+,224C	122c
-..s   AAr   z1.0.0)r   versiontools)r]  r   r   r   )r_  claude_agent_sdkrX  rY  r   )rX  rY  rZ  r`  rb  r_  s        @r   build_serverrg    s    <	+P C
(*3*3 
.$ S
)	+
	6+
	6 
,
O
// !W,
35 5r   )r   r   )NT)r   r   rL   zdict | NonerS   r   r   ztuple[str, int])r   r   r   r   )r   r   r   r   )r   r   r   r   r   r
   )r   r   r   r   )r   r   r   zset[str])r   r   r   floatr   r   )r   r   )r   r   r   r   )r   r   r   r   )r   r   r  r   r   r   )r   r   r   r   )rr   )r$  r   r   r   r   r   )r   r   r   r   r   r   )r   r   )r   r   r   r   )r   r   r   r   r   r   )r   r   r   r   r   r   r   r   r   r   r   r   )r   r   r   r   )r   r   r   r   r   r   )Frr   )r   r   r   r   rP  r   rQ  r   r   r   r   ztuple[bool, str])rU  r   r   r   )Br   
__future__r   r   r   rE  rC  r   r   r   r   r   r   r   html.parserr	   pathlibr
   voice.eventsr   r   homer   r   __annotations__r   r   r   rZ   r[   re   ry   rx   compileIr{   r   r   r   rG   r   r   r   r   r   r   r   r   r   r  r  r  r  r   r'  r*  r,  r1  r   r6  r=  rI  rK  rN  rV  rg  ro   r   r   <module>rq     s  > # 	 	  
 2 2 !  "  yq!"499;"]2 / 	 >I
$		4.
J	<bdd	Cbjj=rttDRZZR

"**Z
 m m` 48'+% $%0?%<&#	N@
"MD0	D/<
*%6 #&2" EJ%% 0%2845r   