
    GVjJ                    |   U d Z ddlmZ ddlZddlZddlmZ ddlmZ ddl	m
Z
  e
e      j                         j                  j                  ZddgZi Zd	ed
<   d#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d,dZd-dZd.dZd/dZd0dZd1d2dZ dZ!d3dZ"d4dZ#d5dZ$d6dZ%d7d8dZ&d9d Z'	 d:	 	 	 d;d!Z(d" Z)y)<u  Gmail (send/read/search/reply) in-process MCP for Jarvis — MULTI-ACCOUNT.

Gives Claude real Gmail hands across more than one Google account: Ahmed's
work mailbox (alrugaibfurniture.com) and his business Gmail, chosen per call
by a spoken label ("work" / "business"). "Deal with my work email" and
"reply to that on my business account" route to the right mailbox.

Same OAuth mechanism as ``gcal_control`` — the SAME desktop OAuth client
(``google_oauth.json`` at the project root), just with Gmail scopes and a
SEPARATE cached token PER ACCOUNT (``google_gmail_<label>.json``). The first
action on an account opens a browser once; sign in with THAT account (work vs
business) and the token caches + refreshes silently forever after. No API key,
no per-call billing.

Runs IN-PROCESS via the Agent SDK MCP server (key ``"gmail"``; Claude sees
``mcp__gmail__<name>``). Google client libs are imported lazily so importing
this module never hard-fails when they're missing.

Config (``.env`` / env):
  GMAIL_ACCOUNTS    comma-separated labels to offer, default "work,business"
  GMAIL_DEFAULT     label used when a tool call omits ``account``, default
                    the first in GMAIL_ACCOUNTS ("work")
  GOOGLE_OAUTH_JSON shared desktop OAuth client (default google_oauth.json)
Set ``MCP_GMAIL=0`` to disable. Enabling ALSO requires the Gmail API turned on
in the same Google Cloud project as Calendar and the ``gmail.*`` scopes added
to that project's OAuth consent screen (see README).
    )annotationsN)EmailMessage)getaddresses)Pathz,https://www.googleapis.com/auth/gmail.modifyz*https://www.googleapis.com/auth/gmail.sendzdict[str, tuple]_SVCc                     t         j                  j                  dd      } | j                  d      D cg c]1  }|j	                         s|j	                         j                         3 c}S c c}w )NGMAIL_ACCOUNTSzwork,business,)osenvirongetsplitstriplower)rawls     A/Users/ahmed/devFolder/Ultron/claude-voice/voice/gmail_control.py_labelsr   2   sL    
**..)?
;C'*yy~C~!AGGIOO~CCCs   A-
 A-c                     t         j                  j                  dd      j                         j	                         } t               }| |v r| S |r|d   S dS )NGMAIL_DEFAULT r   work)r   r   r   r   r   r   )dlabelss     r   _default_labelr   7   sK    


+11399;AYFV1Bv&)B6B    c                 P    t         t        j                  j                  dd      z  S )NGOOGLE_OAUTH_JSONzgoogle_oauth.json)_ROOTr   r   r    r   r   _oauth_pathr!   =   s    2::>>"57JKKKr   c                    t         d|  dz  S )Ngoogle_gmail_z.json)r   )labels    r   _token_pathr%   A   s    ]5'///r   c                 z    t         j                  j                  dd      dk(  ryt               j	                         S )zWhether the Gmail MCP should be exposed to Claude. On when not disabled
    and the shared OAuth client exists (individual accounts consent on first
    use).	MCP_GMAIL10F)r   r   r   r!   existsr    r   r   enabledr+   E   s/     
zz~~k3'3.=!!r   c                    dd| dgiS )z2Wrap a plain string as an MCP text content result.contenttext)typer.   r    msgs    r   _textr2   N   s    5677r   c                    | s
t               S | j                         j                         }t               D ]  }||k(  s	||v s||v s|c S  t               S )zvNormalise a spoken account label to a configured one (default if
    unknown/empty). 'my work'/'work email' -> 'work'.)r   r   r   r   )r$   r   cfgs      r   _resolver5   S   sS     Ay!8sax18J  r   c                   | t         v r	t         |    S ddlm} ddlm} ddlm} ddlm} t        |       }d}|j                         r|j                  t        |      t              }|r|j                  s|r/|j                  r#|j                   r|j#                   |              nYt%        d|  d| j'                          d	       |j)                  t        t+                     t              }|j-                  d
      }|j/                  |j1                                 |dd|d      }|j3                         j5                  d      j7                         j9                  d|       }	||	ft         | <   t         |    S )u   Return (gmail_service, email_address) for one account, running the
    one-time browser consent if that account has no cached token. Blocking —
    call via asyncio.to_thread so the voice loop never stalls.r   )Credentials)InstalledAppFlow)Request)buildNz&  [gmail] first-time sign-in for the 'u4   ' account — a browser will open; sign in with the z# Google account and approve Jarvis.)portgmailv1F)credentialscache_discoveryme)userIdemailAddress)r   google.oauth2.credentialsr7   google_auth_oauthlib.flowr8   google.auth.transport.requestsr9   googleapiclient.discoveryr:   r%   r*   from_authorized_user_filestr_SCOPESvalidexpiredrefresh_tokenrefreshprintupperfrom_client_secrets_filer!   run_local_server
write_textto_jsonusers
getProfileexecuter   )
r$   r7   r8   r9   r:   
token_pathcredsflowsvcaddrs
             r   _servicer\   _   sC    }E{5:6/U#JE55c*owOU]]u':':MM')$:5' B99> H01 2 $<<KM"G-D))q)1Eemmo.
5%
HC99;!!!.668<<D+DK;r   c                d    t        j                  | j                               j                         S )N)base64urlsafe_b64encodeas_bytesdecoder0   s    r   _b64rb      s"    ##CLLN3::<<r   c                @   t        |       \  }}t               }||d<   |r||d<   ||d<   |j                  |       |j                         j	                         j                  ddt        |      i      j                          d| d| |rd	| d
ndz   d|  d| dz   S )NToCcSubjectr@   r   rA   bodyzSent 'z' to  (cc )r   z from your 
 account ().)r\   r   set_contentrT   messagessendrb   rV   )r$   tosubjectrh   ccrZ   r[   r1   s           r   _sendrs      s    IC
.CCI	D	C	NOODIIK5$s),   ..5giWIU2$'BU2$a=BGE7*TF"56 7r   c                    | D ]G  }|j                  dd      j                         |j                         k(  s5|j                  dd      c S  y)Nnamer   value)r   r   )headersru   hs      r   _hdrry      sD    55""$

455"%%  r   c                   t        |       \  }}|j                         j                         j                  d|xs d|      j	                         }|j                  dg       D cg c]  }|d   	 }}|s	d|  d| dS |  d| d	g}|D ]  }	|j                         j                         j                  d|	d
g d      j	                         }|j                  di       j                  dg       }
|j                  dd      d d }|j                  d|	 dt        |
d       dt        |
d       dt        |
d       d| 
        dj                  |      S c c}w )Nr@   in:inboxrA   q
maxResultsrn   idzNo matching messages in the rk   rl   z):metadata)Fromrf   DaterA   r   formatmetadataHeaderspayloadrw   snippetr   d   z[id z] r   z	
  from: r   z	
  subj: rf   z
  
)	r\   rT   rn   listrV   r   appendry   join)r$   querylimitrZ   r[   resmidslinesmidrx   r   s               r   _searchr      st   IC
))+


 
%
%u*
u & >>Egi GGJ3
43q1T73C
4-eWJtfBGGwjb)*EIIK  "&&C
7 ' 99@ 	
 EE)R $$Y3%%	2&t,3%r$q&/**T!V_4E FAy)*$wi9	:  99U 5s   %Ec                   | g}d}|r|j                  d      }|j                  dd      }|j                  di       j                  d      }|dk(  r'|r%t        j                  |      j	                  dd      S |d	k(  r)|r'|s%t        j                  |      j	                  dd      }|j                  |j                  d
g       xs g        |r|rddl}|j                  dd|      S y)zEDepth-first search for a text/plain part; fall back to any text/html.r   r   mimeTyperh   data
text/plainutf-8replace	text/htmlpartsNz<[^>]+>z(no readable body))popr   r^   urlsafe_b64decodera   extendresub)r   stackhtmlpmimer   r   s          r   
_walk_bodyr      s    IED
IIaLuuZ$uuVR $$V,<D++D188)LL;4++D188)LDQUU7B'-2.  vvj"d++r   c                   t        |       \  }}|j                         j                         j                  d|d      j	                         }|j                  di       j                  dg       }t        |j                  di             }t        |      dkD  r|d d dz   }dt        |d	       d
t        |d       dt        |d       dt        |d       d| 
S )Nr@   fullrA   r   r   r   rw   i  u   
…(truncated)zFrom: r   z
To: rd   z

Subject: rf   z
Date: r   z

)r\   rT   rn   r   rV   r   lenry   )r$   r   rZ   r[   r   rx   rh   s          r   _readr      s    IC		""$3v"FNNPA	i  B/AaeeIr*+D
4y4ET{//T!V_%VDDM? ;Q	*+8DFO3DDP Qr   c                   t        |       \  }}|j                         j                         j                  d|dg d      j	                         }|j                  di       j                  dg       }t        |d      }t        |d      }	|	j                         j                  d	      sd
|	z   }	t        |d      }
t        |d      dz   |
z   j                         }t               }||d<   |	|d<   d}|r|j                         ht        |g      D ch c]  \  }}|r|j                          c}}z  }g }t        t        |d      t        |d      g      D ]K  \  }}|s	|j                         |vs|j                  |j                                |j                  |       M dj                  |      }|r||d<   |
r
|
|d<   ||d<   |j                  |       |j                         j                         j                  dt!        |      |j                  d      d      j	                          d| |rd| dndz   d|  d| dz   S c c}}w )zReply in-thread to a message id, preserving threading headers.
    reply_all=True keeps every other recipient (original To + Cc, minus us).r@   r   )r   rd   re   rf   
Message-ID
Referencesr   r   rw   r   rf   zre:zRe: r   r    rd   r   re   , zIn-Reply-TothreadId)r   r   rg   zReplied to ri   rj   z	 on your rk   rl   )r\   rT   rn   r   rV   ry   r   
startswithr   r   r   addr   r   rm   ro   rb   )r$   r   rh   	reply_allrZ   r[   origrx   rp   subjmsg_idrefsr1   rr   _aseenkeep_ns                      r   _replyr      sE    IC99;!%%J' & ( )0	 	 	B##Ir2A	aB9D::<""5)}!\"FL!C'&0779D
.CCIC	N	B 

~!-rd!3!:!3 04q!78 "#!3!: :!44=$q$-"@AEBQWWYd*#A B YYt_CI#M LOODIIK#YDHHZ,@A   CCJ79"2E"Q-2>%
4&34 5#!:s   Ii  @ c                    | xs dj                  dd      j                  dd      }t        j                  |dt        |       dz  z  z         S )z<Decode Gmail's base64url part data (it comes back unpadded).r   -+r   /=   )r   r^   	b64decoder   )r   ss     r   _b64dr     sJ    	S#&..sC8AAAw{ 3344r   c              #     K   | g}|r>|j                  d      }| |j                  |j                  dg       xs g        |r=yyw)zCYield every MIME part of a message, depth-first (payload included).r   r   N)r   r   r   )r   r   r   s      r   _walk_partsr   	  sC     IE
IIaLQUU7B'-2. s   AAAc                    t        | j                  dg       xs g d      j                         }|j                  d      r|j	                  d      r|dd S |S )z9The part's Content-ID with the <angle brackets> stripped.rw   z
Content-ID<>   )ry   r   r   r   endswith)partvs     r   _cidr     sM    TXXi$*L9??AAll3'AJJsO1Qr7BBr   c                d   d\  }}i }g }t        |j                  di       xs i       D ]  }|j                  dd      xs d}|j                  d      r,|j                  di       xs i }|j                  d      }	|j                  d      xs d}
t        |      }|r|j                  d	      r|	}|s|j                  d
      rt	        |j                  d      xs d      t
        k  rn| j                         j                         j                         j                  d|j                  dd      |d
         j                         j                  d      }|r2|t        j                  t        |            j                         d||<   d|
rI|j                  |j                  d
d      xs d|
|t	        |j                  d      xs d      dd       |dk(  r"|	r |t        |	      j                  dd      z  }|dk(  s|	s|t        |	      j                  dd      z  } ||||dS )u   Split one full-format message into its text body, HTML body, inline
    (cid:) images and real attachments. Bodies are NOT truncated — the card
    shows the whole thing, unlike ``_read`` which is a TTS summary.)r   r   r   r   r   z
multipart/rh   r   filenamezimage/attachmentIdsizer   r@   r   rA   	messageIdr   )r   b64)att_idr   r   r   pathr   r   r   r   )	body_text	body_htmlinlineattachments)r   r   r   r   int_INLINE_MAXrT   rn   r   rV   r^   	b64encoder   ra   r   )rZ   r   r.   r   r   attsr   r   rh   r   fnamecidr   s                r   	_parts_ofr     s    JD$ FDy"-34uuZ$*??<(uuVR &Bxxj!'R1g4??8,C488N3(-A.+=iik**,88:>>155r?N+ ? --4WYss6{  '+&,&6&6uSz&B&I&I&KMsKK488NB#?#E2%*D!$TXXf%5%:!;RI J <DE$K&&w	::D[ TE$K&&w	::D7 58 DF! !r   c                    t        |       \  }}|j                         j                         j                  d|xs d|      j	                         }|j                  dg       xs g D cg c]  }|d   	 c}S c c}w )zAMessage ids matching a Gmail query, newest first (empty = inbox).r@   r{   r|   rn   r   )r\   rT   rn   r   rV   r   )r$   r   r   rZ   _addrr   r   s          r   
search_idsr   ?  sz    %JC
))+


 
%
%u*
u & >>Egi  WWZ4::;:AdG:;;;s   )A8c                   t        |       \  }}	 |j                         j                         j                  d|d      j	                         }g }|j                  dg       xs g D ]  }|j                  di       j                  dg       xs g }|j                  |j                  d	d
      t        |d      t        |d      t        |d      t        |d      t        |d      t        |j                  d      xs d      dz  d|j                  d      xs g v dt        ||              |j                  d        t        d |D        d      }|j                  d	|      |||dS # t
        $ r |j                         j                         j                  d|d      j	                         }|j                         j                         j                  d|j                  d|      d      j	                         }Y w xY w)zA whole THREAD as structured data for the HUD card. `ident` is a thread
    id OR any message id inside it (Gmail ids are interchangeable-looking, so
    a message id is resolved to its threadId on the fly). Oldest first.r@   r   r   minimalr   rn   r   rw   r   r   r   rd   re   rf   r   internalDater   g     @@SENTlabelIds)r   fromrp   rr   rq   datetsoutgoingc                    | d   S )Nr   r    )xs    r   <lambda>zthread.<locals>.<lambda>`  s    AdGr   )keyc              3  2   K   | ]  }|d    s	|d      yw)rq   Nr    ).0r   s     r   	<genexpr>zthread.<locals>.<genexpr>a  s     ?$Q!I,AiL$s   

z(no subject))	thread_idaccount_addrrq   rn   )r\   rT   threadsr   rV   	Exceptionrn   r   ry   r   r   sortnext)	r$   identrZ   r[   tr   msgsrx   rq   s	            r   threadr  G  s    ICOIIK!%%E& & 229') 	
 DUU:r"(b(EE)R $$Y39reeD"oFO44=QAy)46?aeeN+0q1F:155#4#:;
 Q
 	 ) 	II#I$?$?PGtU+TD2 2'  OIIK  "&&E) ' 55<WY 	
IIK!%%AEE*e4V & EELWY 	
Os   >E* *BHHc                X   ddl m} t        |       \  }}|j                         j	                         j                         j                  d||      j                         }|j                  |xs ||      }	|	j                  t        |j                  dd                   t        |	      S )zDownload one attachment to ~/.jarvis/attachments/<thread>/ and return
    its local path. The filename is sanitised (no path traversal).r   )	mail_viewr@   r   r   r   )voicer  r\   rT   rn   r   r   rV   	dest_pathwrite_bytesr   rH   )
r$   r   r   r   r   r  rZ   r   r   r   s
             r   
attachmentr	  f  s      %JC		**,00v& 	1 	229') y2FH=DU155,-.t9r   c            
       	 ddl 	ddlm} m} dj	                  t                     xs d}d| dt                d} | d	d
|z   dz   t        t        t        t        t        d      d 	fd       } | dd|z   t        t        t        d      d 	fd       } | dd|z   t        t        d      d 	fd       } | dd|z   t        t        t        d      d 	fd       } | ddi       d d       } |dd|||||g      S )!z5Build and return the in-process ``gmail`` MCP server.r   N)toolcreate_sdk_mcp_serverr   r   z `account` picks the mailbox (z); omit it for 'z'.
gmail_sendz1Send an email from one of Ahmed's Gmail accounts.z8 `to`/`cc` are comma-separated addresses; `cc` optional.)rp   rq   rh   rr   accountc                  K   t        | j                  dd            j                         }|st        d      S 	 j	                  t
        t        | j                  d            |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)	Nrp   r   z gmail_send failed: no recipient.r  rq   rh   rr   zgmail_send failed: )rH   r   r   r2   	to_threadrs   r5   r   )argsrp   outeasyncios       r   r  z build_server.<locals>.gmail_send~  s     
 $#$**,;<<	4))x 34bDHHY+,c$((622F.GDHHT2&'--/1 1C :	1
  	4.qc233	4sG   7C?BC 	C
C C?C 	C<$C71C<2C?7C<<C?gmail_searchzSearch a Gmail account using Gmail search syntax (e.g. 'from:elie is:unread', 'subject:invoice newer_than:7d', empty = inbox). Returns a list with a message id for each; pass an id to gmail_read.)r   r   r  c           
     v  K   	 t        dt        t        | j                  d      xs d      d            }j	                  t
        t        | j                  d            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   r   zgmail_search failed: )
maxminr   r   r  r   r5   rH   r2   r   )r  r   r  r  r  s       r   r  z"build_server.<locals>.gmail_search  s     	63s488G#4#:;R@AE))$((9"56DHHWb)*E3 3C :3  	60455	6sG   B9A>B BB B9B 	B6B1+B6,B91B66B9
gmail_readz@Read the full body of one message by its id (from gmail_search).)r   r  c                J  K   t        | j                  dd            j                         }|st        d      S 	 j	                  t
        t        | j                  d            |       d {   }t        |      S 7 # t        $ r}t        d|       cY d }~S d }~ww xY ww)Nr   r   z!gmail_read failed: no message id.r  zgmail_read failed: )rH   r   r   r2   r  r   r5   r   r  r   r  r  r  s       r   r  z build_server.<locals>.gmail_read  s      $((4$%++-<==	4))x 34c; ;C:;  	4.qc233	4sF   7B#2A? -A=.A? <B#=A? ?	B BB B#B  B#gmail_replyzReply in-thread to a message by its id (from gmail_search), keeping the subject and conversation threading. Goes to the original sender.)r   rh   r  c                ~  K   t        | j                  dd            j                         }|st        d      S 	 j	                  t
        t        | j                  d            |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   z"gmail_reply failed: no message id.r  rh   zgmail_reply failed: )rH   r   r   r2   r  r   r5   r   r  s       r   r  z!build_server.<locals>.gmail_reply  s     
 $((4$%++-=>>	5)))!45sDHHVR()+ +C :+  	5/s344	5sG   7B=AB BB B=B 	B:"B5/B:0B=5B::B=gmail_accountszDList the Gmail accounts Jarvis can use and which one is the default.c           
     
  K   g }t               D ]S  }t        |      j                         }|t               k(  rdnd}|rdnd}|j	                  d| d| d|rd|z   nd        U t        d	j                  |      xs d
      S w)Ndefaultr   z	signed inznot signed in yetz- z (rj   u    — r   zNo accounts configured.)r   r%   r*   r   r   r2   r   )r  r   r   authedmarkstates         r   r   z$build_server.<locals>.gmail_accounts  s      A ^**,F !^%5 592D#)K/BELL2aS5'T7T>r*JKL	 
 TYYu%B)BCCs   BBr<   z1.0.0)ru   versiontools)r  dictreturnr(  )	r  claude_agent_sdkr  r  r   r   r   rH   r   )
r  r  accts	acct_helpr  r  r  r  r   r  s
            @r   build_serverr-  t  s\   <IIgi *FE1% 9#%&b*I 
,
=	
IFGc
MO4	O4 
. $$ ##
68686 
,
L!c24	44	4 
- (+SS!IK
5	K
5 

DD !<[ r   )r)  	list[str])r)  rH   )r)  r   )r$   rH   r)  r   )r)  bool)r1   rH   r)  r(  )r$   z
str | Noner)  rH   )r$   rH   )r1   r   r)  rH   )r$   rH   rp   rH   rq   rH   rh   rH   rr   rH   r)  rH   )rw   r   ru   rH   r)  rH   )r$   rH   r   rH   r   r   r)  rH   )r   r(  r)  rH   )r$   rH   r   rH   r)  rH   )F)
r$   rH   r   rH   rh   rH   r   r/  r)  rH   )r   rH   r)  bytes)r   r(  )r   r(  r)  rH   )r   r(  r)  r(  )   )r$   rH   r   rH   r   r   r)  r.  )r$   rH   r   rH   r)  r(  )r   )r$   rH   r   rH   r   rH   r   rH   r   rH   r)  rH   )*__doc__
__future__r   r^   r   email.messager   email.utilsr   pathlibr   __file__resolveparentr   rI   r   __annotations__r   r   r!   r%   r+   r2   r5   r\   rb   rs   ry   r   r   r   r   r   r   r   r   r   r   r  r	  r-  r    r   r   <module>r;     s   8 #  	 & $ X ''..
 30  D
CL0"8
	J=7( &Q(5b 5/C$!N<2@ !#'*Zr   