
    (Gj                        d Z ddlZddlZddlZddlZddl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 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 dd	lmZ dd
lmZmZmZmZmZ ddlm Z m!Z!m"Z"m#Z#m$Z$m%Z% ddl&m'Z'm(Z( ddl)m*Z*m+Z+  ej,        e-          Z.dZ/d d d d dZ0dZ1dZ2	 dDde3de4de5dede	j	        f
dZ6 G d dej7                  Z8 G d dej9                  Z:h d Z;dEd#Z<d$e3d%e=de>fd&Z?de>fd'Z@d(e3de3fd)ZAd*ejB        jC        de3fd+ZDd,e3de3fd-ZEd(e3de3fd.ZFd$e3de3fd/ZGd0e3d1e3de>fd2ZH ejI        d3ejJ                  ZK ejI        d4ejJ                  ZLd5d6d*ejB        jC        d7e3d8e3dee>e3f         fd9ZM	 dFd*ejB        jC        d;e>deee3ef                  fd<ZN G d= d>e           ZOddd:d?d@ZPde>fdAZQdB ZRdGdCZSdS )Hu  
Email platform adapter for the Hermes gateway.

Allows users to interact with Hermes by sending emails.
Uses IMAP to receive and SMTP to send messages.

Environment variables:
    EMAIL_IMAP_HOST     — IMAP server host (e.g., imap.gmail.com)
    EMAIL_IMAP_PORT     — IMAP server port (default: 993)
    EMAIL_SMTP_HOST     — SMTP server host (e.g., smtp.gmail.com)
    EMAIL_SMTP_PORT     — SMTP server port (default: 587)
    EMAIL_ADDRESS       — Email address for the agent
    EMAIL_PASSWORD      — Email password or app-specific password
    EMAIL_POLL_INTERVAL — Seconds between mailbox checks (default: 15)
    EMAIL_ALLOWED_USERS — Comma-separated list of allowed sender addresses
    N)decode_header)MIMEMultipartMIMEText)MIMEBase
formatdate)encoders)Path)AnyDictListOptionalTuple)BasePlatformAdapterMessageEventMessageType
SendResultcache_document_from_bytescache_image_from_bytes)PlatformPlatformConfig)env_intenv_bool)noreplyzno-replyno_reply
donotreplyzdo-not-replyzmailer-daemon
postmasterbounceznotifications@z
automated@zauto-confirmz
auto-reply
automailerc                 2    |                                  dk    S )Nnolowervs    E/home/rurouni/.hermes/hermes-agent/plugins/platforms/email/adapter.py<lambda>r(   :   s    		T 1     c                 .    |                                  dv S )N>   bulkjunklistr#   r%   s    r'   r(   r(   ;   s    AGGII)AA r)   c                      t          |           S Nboolr%   s    r'   r(   r(   <   s    $q'' r)   c                      t          |           S r/   r0   r%   s    r'   r(   r(   =   s    $q'' r)   )zAuto-Submitted
PrecedencezX-Auto-Response-SuppresszList-UnsubscribeP     hostporttimeoutsource_addressreturnc                    d}t          j        | |t           j        t           j                  D ]\  }}}}}	t          j         |||          }
|
                    |           	 |r|
                    |           |
                    |	           |
c S # t          $ r }|}|
                                 Y d}~d}~ww xY w||t          d|  d|           )a  Create a TCP connection using only IPv4 addresses.

    This mirrors ``socket.create_connection`` but constrains DNS resolution to
    ``AF_INET``.  It avoids mutating process-global socket functions, which
    matters because email sends run in executor threads.
    NzNo IPv4 address found for :)	socketgetaddrinfoAF_INETSOCK_STREAM
settimeoutbindconnectOSErrorclose)r6   r7   r8   r9   
last_errorfamilysocktypeproto
_canonnamesockaddrsockexcs               r'   _create_ipv4_connectionrN   F   s    "&J9?9KdFNF$6: :  5%X }VXu55   	 *		.)))LL"""KKK 	 	 	JJJLLLLLLLL	 
<t<<d<<
=
==s   "-B
B<B77B<c                       e Zd Zd ZdS )	_IPv4SMTPc                 2    t          |||| j                  S )Nr9   )rN   r9   )selfr6   r7   r8   s       r'   _get_socketz_IPv4SMTP._get_socketf   s(    &.	
 
 
 	
r)   N__name__
__module____qualname__rT    r)   r'   rP   rP   e   s#        
 
 
 
 
r)   rP   c                       e Zd Zd ZdS )_IPv4SMTP_SSLc                     t          |||| j                  }| j                            |t	          | d|                    S )NrR   _host)server_hostname)rN   r9   contextwrap_socketgetattr)rS   r6   r7   r8   raw_socks        r'   rT   z_IPv4SMTP_SSL._get_socketp   sX    *.	
 
 
 |''#D'488 ( 
 
 	
r)   NrU   rY   r)   r'   r[   r[   o   s#        

 

 

 

 

r)   r[   >   .gif.jpg.png.jpeg.webpimapimaplib.IMAP4c                     	 	 ddl m} n# t          $ r d}Y nw xY w|                     dd| d           dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)	aR  Send RFC 2971 IMAP ID command identifying this client.

    Required by 163/NetEase mailbox after LOGIN: without it, every UID
    SEARCH/FETCH returns ``BYE Unsafe Login`` and disconnects.  Other
    IMAP servers either honor it silently or reject the unknown command;
    we swallow failures so non-supporting servers keep working.
    r   )__version__0IDz"("name" "hermes-agent" "version" "zE" "vendor" "NousResearch" "support-email" "noreply@nousresearch.com")z([Email] IMAP ID command not accepted: %sN)
hermes_clirk   	Exceptionxatomloggerdebug)rh   _hermes_versiones      r'   _send_imap_idru      s    D	"AAAAAAA 	" 	" 	"!OOO	"

: : : :	
 	
 	
 	
 	
  D D D?CCCCCCCCCDs)   
 8 8 8 
A(A##A(addressheadersc                     |                                  t          fdt          D                       rdS t                                          D ]+\  }}|                    |d          }|r ||          r dS ,dS )z>Return True if this email is from an automated/noreply source.c              3       K   | ]}|v V  	d S r/   rY   ).0patternaddrs     r'   	<genexpr>z'_is_automated_sender.<locals>.<genexpr>   s'      
<
<w7d?
<
<
<
<
<
<r)   T F)r$   any_NOREPLY_PATTERNS_AUTOMATED_HEADERSitemsget)rv   rw   headercheckvaluer|   s        @r'   _is_automated_senderr      s    ==??D

<
<
<
<*;
<
<
<<< t+1133  FB'' 	UU5\\ 	445r)   c                  `   t          j        dd                                          } t          j        dd                                          }t          j        dd                                          }t          j        dd                                          }t          | |||g          S )zCheck if email platform settings are available and non-blank.

    Treats blank/whitespace-only values as missing so an abandoned setup that
    left empty ``EMAIL_*`` keys in ``.env`` does not enable the platform (#40715).
    EMAIL_ADDRESSr~   EMAIL_PASSWORDEMAIL_IMAP_HOSTEMAIL_SMTP_HOST)osgetenvstripall)r|   pwdrh   smtps       r'   check_email_requirementsr      s     9_b))//11D
)$b
)
)
/
/
1
1C9&++1133D9&++1133Dc4&'''r)   rawc                    t          |           }g }|D ]\\  }}t          |t                    r-|                    |                    |pdd                     G|                    |           ]d                    |          S )z<Decode an RFC 2047 encoded email header into a plain string.utf-8replaceerrors )r   
isinstancebytesappenddecodejoin)r   partsdecodedpartcharsets        r'   _decode_header_valuer      s    #EG ! !gdE"" 	!NN4;;w'9');LLMMMMNN4    88Gr)   msgc                    |                                  rQ|                                 D ]}|                                }t          |                    dd                    }d|v r>|dk    rG|                    d          }|r/|                                pd}|                    |d	          c S |                                 D ]}|                                }t          |                    dd                    }d|v r>|d
k    rV|                    d          }|r>|                                pd}|                    |d	          }t          |          c S dS |                     d          }|rV|                                 pd}|                    |d	          }|                                 d
k    rt          |          S |S dS )z?Extract the plain-text body from a potentially multipart email.Content-Dispositionr~   
attachment
text/plainTr   r   r   r   	text/html)	is_multipartwalkget_content_typestrr   get_payloadget_content_charsetr   _strip_html)r   r   content_typedispositionpayloadr   htmltexts           r'   _extract_text_bodyr      s   
 !HHJJ 
	E 
	ED0022Ldhh'<bAABBK{**|++**$*77 E"6688CGG">>')>DDDDDHHJJ 
	- 
	-D0022Ldhh'<bAABBK{**{****$*77 -"6688CGG">>')>DDD&t,,,,,r///.. 	--//:7G>>')><<D##%%44"4(((Krr)   r   c                    t          j        dd| t           j                  }t          j        dd|t           j                  }t          j        dd|t           j                  }t          j        dd|          }t          j        dd	|          }t          j        d
d|          }t          j        dd|          }t          j        dd|          }t          j        dd|          }|                                S )z5Naive HTML tag stripper for fallback text extraction.z	<br\s*/?>
)flagsz<p[^>]*>z</p>z<[^>]+>r~   z&nbsp;r   z&amp;&z&lt;<z&gt;>z\n{3,}

)resub
IGNORECASEr   )r   r   s     r'   r   r      s    6,d"-@@@D6+tT???D6'4R];;;D6*b$''D6)S$''D6(C&&D6'3%%D6'3%%D6)VT**D::<<r)   c                     t          j        d|           }|r9|                    d                                                                          S |                                                                 S )z5Extract bare email address from 'Name <addr>' format.z	<([^>]+)>   )r   searchgroupr   r$   )r   matchs     r'   _extract_email_addressr      s]    IlC((E .{{1~~##%%++---99;;r)   c                     |                      d          \  }}}|                                                                S )z=Return the lowercased domain part of an email address, or ''.@)
rpartitionr   r$   )rv   _domains      r'   
_domain_ofr      s5    %%c**LAq&<<>>!!!r)   abc                 j   | pd                                                                                     d          } |pd                                                                                     d          }| r|sdS | |k    rdS |                     d|z             p|                    d| z             S )aT  Return True if two domains are equal or in an organizational
    parent/subdomain relationship (relaxed DMARC alignment).

    DMARC relaxed alignment treats ``mail.example.com`` as aligned with
    ``example.com``. We approximate organizational alignment by checking
    exact equality or that one domain is a dot-suffix of the other.
    r~   .FT)r   r$   rstripendswith)r   r   s     r'   _domains_alignedr      s     
b!!((--A	
b!!((--A A uAvvt::cAg5!**S1W"5"55r)   z!\b(dmarc|dkim|spf)\s*=\s*([a-z]+)zR\b(header\.from|header\.d|smtp\.mailfrom|smtp\.from|envelope-from)\s*=\s*([^\s;]+)r~   authserv_id	from_addrr   c                   t          |          }|sdS |                     d          pg }|sdS d}|D ]}d                    t          |                                                    }|ri|                    dd          d                                                                         }t          ||          s||                                k    r|} |d	S d
 t          	                    |          D             }	d t          	                    |          D             }
|	                    d          dk    rdS |	                    d          dk    rvt          |
                    dd                    p+|
                    dd          p|
                    dd          }d|v rt          |          n|}t          ||          rdS |	                    d          dk    rK|
                    dd          p"t          |
                    dd                    }t          ||          rdS dd|dd          dfS )u  Verify that the message's ``From:`` domain is authenticated.

    The ``From:`` header is attacker-controlled and is never authenticated by
    IMAP delivery, so an allowlist keyed on ``From:`` alone is trivially
    spoofable (GHSA-rxqh-5572-8m77). The only trustworthy signal is the
    ``Authentication-Results`` header that the *receiving* mail server (the one
    we IMAP into) stamps after running SPF/DKIM/DMARC. That header is prepended
    by our own server, so the topmost instance is the one we trust; any
    ``Authentication-Results`` an attacker injected into the body of their
    message sorts below it.

    Returns ``(authenticated, reason)``. ``authenticated`` is True when:
      * a DMARC pass is recorded for the From domain, OR
      * an SPF pass aligned with the From domain, OR
      * a DKIM pass aligned (``header.d``) with the From domain.

    When no ``Authentication-Results`` header is present at all, we return
    ``(False, "no Authentication-Results header")`` — fail-closed. Operators
    whose mail server does not stamp this header can opt out of the check
    (see ``EmailAdapter._require_authenticated_sender``).
    )Fzmissing From domainzAuthentication-Results)Fz no Authentication-Results headerNr   ;r   r   )Fz2no Authentication-Results from trusted authserv-idc                 b    i | ],\  }}|                                 |                                 -S rY   r#   )rz   mrs      r'   
<dictcomp>z1_verify_sender_authentication.<locals>.<dictcomp>N  s.    QQQ1qwwyy!''))QQQr)   c                     i | ]?\  }}|                                 |                                                    d           @S )")r$   r   )rz   pr&   s      r'   r   z1_verify_sender_authentication.<locals>.<dictcomp>O  s:    XXXAQWWYY		,,XXXr)   dmarcpass)Tz
dmarc=passspfzsmtp.mailfromr~   z	smtp.fromzenvelope-fromr   )Tzspf=pass aligneddkimzheader.dzheader.from)Tzdkim=pass alignedFzauthentication failed (x   ))r   get_allr   r   splitr   r$   r   _AUTH_METHOD_REfindall_AUTH_PROP_REr   )r   r   r   from_domainrw   trustedr   r   servmethodsprops
spf_domaindkim_domains                r'   _verify_sender_authenticationr     sp   6 Y''K ,++ kk2339rG 988G  S))** 	;;sA&&q)//117799D#D+66 4;CTCTCVCV;V;VJJQQ0G0G0P0PQQQGXX9N9Nw9W9WXXXE {{7v%%!! {{5V##		/2 > >?? ,599D
 D
 ,YY++ 	 03j/@/@Z
+++j
J44 	,++ {{6f$$ii
B//[:eiiWY>Z>Z3[3[K55 	-,,<GDSDM<<<<<r)   Fskip_attachmentsc                 *   g }|                                  s|S |                                 D ]d}t          |                    dd                    }|r	d|v sd|v r1d|vrd|vr:|                                }|dv rd|vrW|                                }|rt          |          }n|                                pd}d| }|                    d	          }|st          |          j
                                        }|t          v rZ	 t          ||          }	n,# t          $ r t                              d
|           Y w xY w|                    |	|d|d           :t%          ||          }	|                    |	|d|d           f|S )zExtract attachment metadata and cache files locally.

    When *skip_attachments* is True, all attachment/inline parts are ignored
    (useful for malware protection or bandwidth savings).
    r   r~   r   inline>   r   r   binzattachment.Tr   z6Skipping non-image attachment %s (invalid magic bytes)image)pathfilenametype
media_typedocument)r   r   r   r   r   get_filenamer   get_content_subtyper   r   suffixr$   _IMAGE_EXTSr   
ValueErrorrq   rr   r   r   )
r   r   attachmentsr   r   r   r   extr   cached_paths
             r'   _extract_attachmentsr  j  s    K 

 * *$((#8"==>> 	!<!<K@W@W{**x{/J/J,,..666<{;Z;Z$$&& 	++H55HH**,,5C*S**H""$"// 	8nn#))+++4WcBB   UW_``` #$*	        4GXFFK#$"*	        s   D%EEc                       e Zd ZdZdef fdZd-dZdej        fdZ	dd	d
e
de
fdZd-dZd-dZd-dZdeeeef                  fdZede
fd            Zede
fd            Zdeeef         ddfdZ	 	 d.dededee         deeeef                  def
dZdefdZ	 d/dededee         defdZd/dedeeeef                  ddfdZ	 	 	 d0deded ee         dee         deeeef                  defd!Z	 	 d1ded#eeeef                  deeeef                  d$e ddf
 fd%Z!deded&ee         defd'Z"	 	 	 d0ded(ed ee         d)ee         dee         defd*Z#	 d/deded(ed)ee         def
d+Z$dedeeef         fd,Z% xZ&S )2EmailAdapterz;Email gateway adapter using IMAP (receive) and SMTP (send).configc                    t                                          |t          j                   |j        pi }t          j        dd          p|                    dd                                          | _	        t          j        dd          | _
        t          j        dd          p|                    dd                                          | _        t          dd          | _        t          j        d	d          p|                    d
d                                          | _        t          dd          | _        t          dd          | _        |                    dd          | _        d|v rt%          |d                   | _        nt)          dd          rd| _        nd| _        |                    dd          pt          j        dd                                                                          | _        t/                      | _        d| _        d | _        i | _        t8                              d| j	                   d S )Nr   r~   rv   r   r   	imap_hostEMAIL_IMAP_PORTi  r   	smtp_hostEMAIL_SMTP_PORTK  EMAIL_POLL_INTERVAL   r   Frequire_authenticated_senderEMAIL_TRUST_FROM_HEADERTr   EMAIL_AUTHSERV_IDi  z"[Email] Adapter initialized for %s)super__init__r   EMAILextrar   r   r   r   _address	_password
_imap_hostr   
_imap_port
_smtp_host
_smtp_port_poll_interval_skip_attachmentsr1   _require_authenticated_senderr   r$   _authserv_idset
_seen_uids_seen_uids_max
_poll_task_thread_contextrq   info)rS   r
  r  	__class__s      r'   r  zEmailAdapter.__init__  s   000 "?B77S599YPR;S;SZZ\\#3R889%6;;YuyyVX?Y?Y``bb!"3S999%6;;YuyyVX?Y?Y``bb!"3S99%&;R@@ "'+=u!E!E  *U2215e<Z6[1\1\D../77 	616D..15D. IImR((NBI6I2,N,N
%''%%'' 	
  #uu#'26 ;=8$-HHHHHr)   r:   Nc                    t          | j                  | j        k    rdS 	 t          | j        d           }| j        dz  }t	          || d                   | _        t
                              dt          | j                             dS # t          t          f$ r; t	          t          | j                  | j         dz  d                   | _        Y dS w xY w)ua  Keep only the most recent UIDs to prevent unbounded memory growth.

        IMAP UIDs are monotonically increasing integers. When the set grows
        beyond the cap, we keep only the highest half — old UIDs are safe to
        drop because new messages always have higher UIDs and IMAP's UNSEEN
        flag prevents re-delivery regardless.
        Nc                      t          |           S r/   )int)us    r'   r(   z.EmailAdapter._trim_seen_uids.<locals>.<lambda>  s    A r)   )key   z'[Email] Trimmed seen UIDs to %d entries)
lenr%  r&  sortedr$  rq   rr   r  	TypeErrorr-   )rS   sorted_uidskeeps      r'   _trim_seen_uidszEmailAdapter._trim_seen_uids  s     t4#666F	U 6F6FGGGK&!+D!+teff"566DOLLBCDXDXYYYYYI& 	U 	U 	U!$t"7"79L8LPQ8Q8R8R"STTDOOOO	Us   A+B ACCc                 H   t          j                    | j        | j        dddt          dt
          j        ffd}	  |            S # t          j        t          t          t          f$ r1}t          |t           j                  r  |d          cY d}~S d}~ww xY w)u  Create an SMTP connection, selecting the correct protocol for the port.

        Port 465 uses implicit TLS (``SMTP_SSL``).  All other ports use
        ``SMTP`` + ``STARTTLS``.

        When the host resolves to an IPv6 address that is unreachable
        (common on networks without IPv6 routing), the default connection can
        hang until the socket timeout expires.  We retry connection-level
        failures through an IPv4-only socket path, without mutating global
        resolver state.  TLS verification errors are not retried.

        Returns a connected SMTP object with TLS established — callers
        can proceed directly to ``login()``.
        F)	ipv4_onlyr8  r:   c                 ,   | rt           nt          j        }| rt          nt          j        }dk    r |t
                    S  |t
                    }	 |                               n## t          $ r |                                  w xY w|S )zAttempt one SMTP connection.i  )r8   r_   r8   r_   )	rP   smtplibSMTPr[   SMTP_SSLSMTP_CONNECT_TIMEOUTstarttlsro   rE   )r8  smtp_clssmtp_ssl_clsr   ctxr6   r7   s       r'   _connectz,EmailAdapter._connect_smtp.<locals>._connect  s    $-?yy7<H,5K==7;KLs{{#|D$8LVYZZZZ8D$0DEEEDc****   

 Ks   A1 1 BTN)sslcreate_default_contextr  r  r1   r<  r=  r=   r8   TimeoutErrorConnectionErrorrD   r   SSLError)rS   rD  rM   rC  r6   r7   s      @@@r'   _connect_smtpzEmailAdapter._connect_smtp  s     (***/ 	 	 	4 	GL 	 	 	 	 	 	 	 		,8::owG 	, 	, 	,#s|,,  8d+++++++++	,s   	A "B!0&BB!B!F)is_reconnectrK  c                  K   d d| j         fd| j        fd| j        fd| j        ffD             }|rPdd                    |          z   dz   }t
                              d	|           |                     d
|d           dS 	 t          j	        | j        | j
        d          }|                    | j         | j                   t          |           |                    d           |                    ddd          \  }}|dk    rA|r?|d         r7|d                                         D ]}| j                            |           |                                  |                                 t
                              dt+          | j                             n3# t,          $ r&}t
                              d|           Y d}~dS d}~ww xY w	 |                                 }		 |	                    | j         | j                   |	                                 n# |	                                 w xY wt
                              d           n3# t,          $ r&}t
                              d|           Y d}~dS d}~ww xY wd| _        t5          j        |                                           | _        t=          d| j                     dS )z>Connect to the IMAP server and start polling for new messages.c                     g | ]	\  }}||
S rY   rY   )rz   namer   s      r'   
<listcomp>z(EmailAdapter.connect.<locals>.<listcomp>,  s4     	
 	
 	
e 	
	
 	
 	
r)   r   r   r   r   u   Not configured — missing z, zL. Set it via `hermes gateway setup` (env) or platforms.email in config.yaml.z
[Email] %semail_missing_configurationF)	retryabler5   r:  INBOXr   NALLOKr   zB[Email] IMAP connection test passed. %d existing messages skipped.z"[Email] IMAP connection failed: %sz$[Email] SMTP connection test passed.z"[Email] SMTP connection failed: %sTz[Email] Connected as )r  r  r  r  r   rq   error_set_fatal_errorimaplib	IMAP4_SSLr  loginru   selectuidr   r%  addr6  logoutr)  r1  ro   rJ  quit_runningasynciocreate_task
_poll_loopr'  print)
rS   rK  missingmessagerh   statusdatar[  rt   r   s
             r'   rC   zEmailAdapter.connect'  s     
	
 	
 !$-0!4>2"DO4"DO4	 	
 	
 	
  	-))G$$%""  LLw///
 !!-w% "    5	$T_dorRRRDJJt}dn555$KK   88HdE::LFD~~$~47~7==?? - -CO'',,,,  """KKMMMKK\^abfbq^r^rssss 	 	 	LL=qAAA55555	
	%%''D

4=$.999				KK>???? 	 	 	LL=qAAA55555	 !-doo.?.?@@5dm55666tsI   DF 
G(G		GI ' H I H22I 
J I;;J c                    K   d| _         | j        rD| j                                         	 | j         d{V  n# t          j        $ r Y nw xY wd| _        t
                              d           dS )zStop polling and disconnect.FNz[Email] Disconnected.)r_  r'  cancelr`  CancelledErrorrq   r)  rS   s    r'   
disconnectzEmailAdapter.disconnectk  s      ? 	#O""$$$o%%%%%%%%)   "DO+,,,,,s   9 A
Ac                 &  K   | j         r	 |                                  d{V  nC# t          j        $ r Y dS t          $ r%}t
                              d|           Y d}~nd}~ww xY wt          j        | j                   d{V  | j         dS dS )z0Poll IMAP for new messages at regular intervals.Nz[Email] Poll error: %s)	r_  _check_inboxr`  rj  ro   rq   rU  sleepr   )rS   rt   s     r'   rb  zEmailAdapter._poll_loopw  s      m 	5:'')))))))))))    : : :5q99999999:- 3444444444 m 	5 	5 	5 	5 	5s   & A&	A&A!!A&c                    K   t          j                    }|                    d| j                   d{V }|D ]}|                     |           d{V  dS )z2Check INBOX for unseen messages and dispatch them.N)r`  get_running_looprun_in_executor_fetch_new_messages_dispatch_message)rS   loopmessagesmsg_datas       r'   rn  zEmailAdapter._check_inbox  s       '))--dD4LMMMMMMMM  	3 	3H((2222222222	3 	3r)   c                    g }	 t          j        | j        | j        d          }	 |                    | j        | j                   t          |           |                    d           |	                    ddd          \  }}|dk    s
|r|d         s'|	 |
                                 S # t          $ r Y S w xY w|d                                         D ]}|| j        v r| j                            |           t          | j                  | j        k    r|                                  |	                    d	|d
          \  }}|dk    ry	 |d         d         }n2# t$          t&          f$ r t(                              d|           Y w xY wt-          |t.          t0          f          st(                              d|           t3          j        |          }|                    dd          }	t9          |	          }
t;          |	          }d|v r@|                    d          d                                                             d          }t;          |                    dd                    }|                    dd          }|                    dd          }t?          |                                           }tC          |
|          rt(          "                    d|
           tG          ||
| j$                  \  }}tK          |          }tM          || j'                  }|(                    ||
|||||||                    dd          ||d           	 	 |
                                 n:# t          $ r Y n.w xY w# 	 |
                                 w # t          $ r Y w w xY wxY wn2# t          $ r%}t(          )                    d|           Y d}~nd}~ww xY w|S )z?Fetch new (unseen) messages from IMAP. Runs in executor thread.r5   r:  rR  r   NUNSEENrT  r   fetchz(RFC822)r   z?[Email] Unexpected IMAP response structure for UID %s, skippingz3[Email] Non-bytes IMAP payload for UID %s, skippingFromr~   r   r   Subjectz(no subject)
Message-IDIn-Reply-Toz%[Email] Skipping automated sender: %sr   )r   Date)r[  sender_addrsender_namesubject
message_idin_reply_tobodyr  datesender_authenticatedauth_reasonz[Email] IMAP fetch error: %s)*rW  rX  r  r  rY  r  r  ru   rZ  r[  r]  ro   r   r%  r\  r1  r&  r6  
IndexErrorr3  rq   warningr   r   	bytearray	email_libmessage_from_bytesr   r   r   r   dictr   r   rr   r   r#  r   r  r!  r   rU  )rS   resultsrh   rf  rg  r[  rw  	raw_emailr   
sender_rawr  r  r  r  r  msg_headersr  r  r  r  rt   s                        r'   rs  z EmailAdapter._fetch_new_messages  sI   \	<$T_dorRRRDX

4=$.999d###G$$$#xx$AAT>>>T!W>"\KKMMMM    D]  7==?? J JCdo-- O'',,,4?++d.AAA,,...'+xxj'I'I$FH~~ !$,QKN		&	2 ! ! !]   !! &i%1CDD !QSV   !#6yAAC!$!4!4J"8"D"DK"6z"B"BKk))&1&7&7&<&<Q&?&E&E&G&G&M&Mc&R&R23779n3U3UVVG!$r!:!:J"%''-"<"<K"&syy{{"3"3K+KEE !%LkZZZ  9V[d6G9 9 95(+ .c22D"6sTMc"d"d"dKNN"'2'2#*&0'2 $'2 # 3 30D'2$ $    }JXKKMMMM    DKKMMMM    D 	< 	< 	<LL7;;;;;;;;	<s   !N9 A/N B++
B85N9 7B88N9 ;BN EN ,F
N FGN )M> =N9 >
NN9 
NN9 N5N%$N5%
N2/N51N22N55N9 9
O(O##O(c                      h d} t          j        dd                                                                          | v p:t          j        dd                                                                          | v S )aJ  Return True when the operator opted into accepting any sender.

        Mirrors the gateway authz allow-all resolution: the per-platform
        EMAIL_ALLOW_ALL_USERS flag or the global GATEWAY_ALLOW_ALL_USERS flag.
        When either is set, sender identity is moot, so the From: authentication
        gate is skipped.
        >   1yestrueEMAIL_ALLOW_ALL_USERSr~   GATEWAY_ALLOW_ALL_USERS)r   r   r   r$   )truthys    r'   _allow_all_senderszEmailAdapter._allow_all_senders  st     &%%I-r2288::@@BBfL Ry2B77==??EEGG6Q	
r)   c                      t          t          j        dd                                          p&t          j        dd                                                    S )u  Return True when a sender allowlist gates email access.

        Authorization keys on the From: address only when an allowlist is
        configured — the per-platform EMAIL_ALLOWED_USERS or the global
        GATEWAY_ALLOWED_USERS. When neither is set the gateway default-denies
        every sender regardless, so the spoofable From: identity grants nothing
        and the authentication gate is unnecessary.
        EMAIL_ALLOWED_USERSr~   GATEWAY_ALLOWED_USERS)r1   r   r   r   rY   r)   r'   _allowlist_in_effectz!EmailAdapter._allowlist_in_effect  sS     I+R006688 >y0"55;;==
 
 	
r)   rw  c           
        K   |d         }|| j                                         k    rdS t          |i           rt                              d|           dS t          j        dd                                          }|st          j        dd                                                                          dvrXt          j        dd                                                                          dvrt                              d	|           dS nRd
 |                    d          D             }|                                |vrt                              d|           dS | j	        rp| 
                                r\|                                 sH|                    dd          s2t                              d||                    dd                     dS |d         }|d                                         }|d         }|}|r|                    d          sd| d| }g }	g }
t          j        }|D ]y}|	                    |d                    |
                    |d                    |d         dk    r|t          j        k    rt          j        }a|d         dk    rt          j        }z||d         d| j        |<   |                     ||d         p|d ||d         p|!          }t-          |pd"|||d         |	|
|d#         pd$          }t                              d%||           |                     |           d{V  dS )&z<Convert a fetched email into a MessageEvent and dispatch it.r  Nz1[Email] Dropping automated sender at dispatch: %sr  r~   r  >   r  r  r  r  uh   [Email] Dropping sender at dispatch — EMAIL_ALLOWED_USERS is unset and open access is not opted in: %sc                     h | ]<}|                                 |                                                                 =S rY   )r   r$   )rz   r|   s     r'   	<setcomp>z1EmailAdapter._dispatch_message.<locals>.<setcomp>(  s>    ___RVR\R\R^R^_tzz||))++___r)   ,z7[Email] Dropping non-allowlisted sender at dispatch: %sr  Fz[Email] Dropping sender with unauthenticated From: %s (%s). If your mail server does not stamp Authentication-Results, set platforms.email.require_authenticated_sender: false (or EMAIL_TRUST_FROM_HEADER=true) to accept the risk.r  z
no verdictr  r  r  Re:z
[Subject: z]

r   r   r   r   r   r  )r  r  r  dm)chat_id	chat_name	chat_typeuser_id	user_namez(empty email)r  )r   message_typesourcer  
media_urlsmedia_typesreply_to_message_idz[Email] New message from %s: %s)r  r$   r   rq   rr   r   r   r   r   r"  r  r  r   r  
startswithr   TEXTr   PHOTODOCUMENTr(  build_sourcer   r)  handle_message)rS   rw  r  allowed_rawallowedr  r  r  r   r  r  msg_typeattr  events                  r'   rt  zEmailAdapter._dispatch_message
  s     }- $---////F  R00 	LLLkZZZF i 5r::@@BB 	y0"55;;==CCEEMaaa	3R88>>@@FFHHPddd:  
 __8I8I#8N8N___G  ""'11VXcddd .	))++	 ++--	 LL!7??		 NND ]L99   F9%%%''}-  	57--e44 	5444d44D 
# 	0 	0Cc&k***s<01116{g%%(k6F*F*F&,V
** '/ "<0-
 -
[)
 ""}-<}-< # 
 
 (!-!# ( 7 ?4
 
 
 	5{GLLL!!%(((((((((((r)   r  contentreply_tometadatac                 :  K   	 t          j                    }|                    d| j        |||           d{V }t	          d|          S # t
          $ rD}t                              d||           t	          dt          |                    cY d}~S d}~ww xY w)z)Send an email reply to the given address.NTsuccessr  z[Email] Send failed to %s: %sFr  rU  )	r`  rq  rr  _send_emailr   ro   rq   rU  r   )rS   r  r  r  r  ru  r  rt   s           r'   sendzEmailAdapter.send}  s      	;+--D#33d&(         J dzBBBB 	; 	; 	;LL8'1EEEe3q66:::::::::	;s   AA 
B9BBBc                 ^    d| j         v r#| j                             dd          d         pdS dS )zDomain part for generated Message-IDs.

        EMAIL_ADDRESS may lack an ``@`` (misconfiguration); fall back to
        ``localhost`` instead of crashing send with an IndexError.
        r   r   	localhost)r  rsplitrk  s    r'   _message_id_domainzEmailAdapter._message_id_domain  s8     $-=''Q//3B{B{r)   to_addrr  reply_to_msg_idc                    t                      }| j        |d<   ||d<   | j                            |i           }|                    dd          }|                    d          sd| }||d<   |p|                    d          }|r
||d	<   ||d
<   t          d          |d<   dt          j                    j        dd          d| 	                                 d}||d<   |
                    t          |dd                     |                                 }		 |	                    | j        | j                   |	                    |           	 |	                                 nb# t"          $ r |	                                 Y nBw xY w# 	 |	                                 w # t"          $ r |	                                 Y w w xY wxY wt&                              d||           |S )z0Send an email via SMTP. Runs in executor thread.r{  Tor  Hermes Agentr  Re: r|  r  r~  
ReferencesT	localtimer  <hermes-N   r   r   r}  plainr   z&[Email] Sent reply to %s (subject: %s))r   r  r(  r   r  r	   uuiduuid4hexr  attachr   rJ  rY  r  send_messager^  ro   rE   rq   r)  )
rS   r  r  r  r   rC  r  original_msg_idmsg_idr   s
             r'   r  zEmailAdapter._send_email  s    oomFD	 "&&w33'')^44!!%(( 	'&W&&G I *BSWW\-B-B 	0!0C /C 4000FPDJLL,SbS1PPD4K4K4M4MPPP"L

8D'733444!!##	JJt}dn555c"""		   

		   

 	<gwOOOsB   5F
 E& &FF
GF! G!G?GGGc                 
   K   dS )u(   Email has no typing indicator — no-op.NrY   )rS   r  r  s      r'   send_typingzEmailAdapter.send_typing  s
        r)   	image_urlcaptionc                 |   K   |pd}|d| z  }|                      ||                                |           d{V S )zSend an image URL as part of an email body.

        ``metadata`` is accepted to honor the base-class contract; the
        email body send doesn't use it.
        r~   z	

Image: N)r  r   )rS   r  r  r  r  r  r   s          r'   
send_imagezEmailAdapter.send_image  sT       }")i)))YYw

h?????????r)           imageshuman_delayc                   K   |sdS ddl m} g }g }|D ]\  }}	|	r|                    |	           |                    d          rf ||dd                   }
t	          |
                                          r|                    |
           {t                              d|
           |                    d|            |s|sdS d                    |          }	 t          j
                    }|                    d| j        |||           d{V  dS # t          $ rR}t                              d	|d
           t                                          ||||           d{V  Y d}~dS d}~ww xY w)u[  Send a batch of images as a single email with multiple MIME attachments.

        Local files are attached directly. URL images have their URL
        appended to the body (email adapter does not download remote
        images). No hard cap — email clients handle dozens of
        attachments fine, subject to SMTP message size limits.
        Nr   )unquotezfile://   z"[Email] Skipping missing image: %szImage: r   z1[Email] Multi-image send failed, falling back: %sT)exc_info)urllib.parser  r   r  r   existsrq   r  r   r`  rq  rr  _send_email_with_attachmentsro   rU  r  send_multiple_images)rS   r  r  r  r  _unquote
body_partslocal_pathsr  alt_text
local_pathr  ru  rt   r*  s                 r'   r  z!EmailAdapter.send_multiple_images  s	       	F444444 "
!##) 	9 	9Ix ,!!(+++##I.. 9%Xim44

##**,, U&&z2222NN#GTTTT !!"7I"7"78888 	: 	F{{:&&	W+--D&&1            	W 	W 	WLLLaZ^L___''..w+VVVVVVVVVVVVVVV	Ws    7D 
E5#AE00E5
file_pathsc                    t                      }| j        |d<   ||d<   | j                            |i           }|                    dd          }|                    d          sd| }||d<   |                    d          }|r
||d	<   ||d
<   t          d          |d<   dt          j                    j        dd          d| 	                                 d}||d<   |r$|
                    t          |dd                     |D ]}	t          |	          }
	 t          |
d          5 }t          dd          }|                    |                                           t#          j        |           |                    dd|
j                    |
                    |           ddd           n# 1 swxY w Y   # t*          $ r&}t,                              d|	|           Y d}~d}~ww xY w|                                 }	 |                    | j        | j                   |                    |           	 |                                 nb# t*          $ r |                                 Y nBw xY w# 	 |                                 w # t*          $ r |                                 Y w w xY wxY wt,                              d|t?          |                     |S )z6Send an email with multiple file attachments via SMTP.r{  r  r  r  r  r  r|  r  r~  r  Tr  r  r  Nr  r   r   r}  r  r   rbapplicationoctet-streamr   attachment; filename=z[Email] Failed to attach %s: %sz4[Email] Sent multi-attachment email to %s (%d files)) r   r  r(  r   r  r	   r  r  r  r  r  r   r   openr   set_payloadreadr
   encode_base64
add_headerrN  ro   rq   r  rJ  rY  r  r  r^  rE   r)  r1  )rS   r  r  r  r   rC  r  r  r  	file_pathr   fr   rt   r   s                  r'   r  z)EmailAdapter._send_email_with_attachments  sM    oomFD	"&&w33'')^44!!%(( 	'&W&&G I'',// 	0!0C /C 4000FPDJLL,SbS1PPD4K4K4M4MPPP"L 	9JJxgw77888# 
	P 
	PIYAP!T]] %a#M>BBD$$QVVXX...*4000OO$9;[STSY;[;[\\\JJt$$$% % % % % % % % % % % % % % %  P P P@)QOOOOOOOOP !!##	JJt}dn555c"""		   

		   

 	JGUXYcUdUdeees   G*A?F5)G5F9	9G<F9	=G
G1G,,G1	5I8 ?I I54I58J3:JJ3J0-J3/J00J3r  	file_namec                 >  K   	 t          j                    }|                    d| j        ||pd||           d{V }t	          d|          S # t
          $ rC}	t                              d|	           t	          dt          |	                    cY d}	~	S d}	~	ww xY w)z#Send a file as an email attachment.Nr~   Tr  z [Email] Send document failed: %sFr  )	r`  rq  rr  _send_email_with_attachmentr   ro   rq   rU  r   )
rS   r  r  r  r  r  kwargsru  r  rt   s
             r'   send_documentzEmailAdapter.send_documentB  s      	;+--D#3302         J dzBBBB 	; 	; 	;LL;Q???e3q66:::::::::	;s   A
A 
B8BBBc                    t                      }| j        |d<   ||d<   | j                            |i           }|                    dd          }|                    d          sd| }||d<   |                    d          }|r
||d	<   ||d
<   t          d          |d<   dt          j                    j        dd          d| 	                                 d}	|	|d<   |r$|
                    t          |dd                     t          |          }
|p|
j        }t          |
d          5 }t          dd          }|                    |                                           t%          j        |           |                    dd|            |
                    |           ddd           n# 1 swxY w Y   |                                 }	 |                    | j        | j                   |                    |           	 |                                 nb# t4          $ r |                                 Y nBw xY w# 	 |                                 w # t4          $ r |                                 Y w w xY wxY w|	S )z.Send an email with a file attachment via SMTP.r{  r  r  r  r  r  r|  r  r~  r  Tr  r  r  Nr  r   r   r}  r  r   r  r  r  r   r  )r   r  r(  r   r  r	   r  r  r  r  r  r   r   rN  r  r   r  r  r
   r  r  rJ  rY  r  r  r^  ro   rE   )rS   r  r  r  r  r   rC  r  r  r  r   fnamer  r   r   s                  r'   r   z(EmailAdapter._send_email_with_attachment[  s    oomFD	"&&w33'')^44!!%(( 	'&W&&G I'',// 	0!0C /C 4000FPDJLL,SbS1PPD4K4K4M4MPPP"L 	9JJxgw77888 OO#QV!T]] 	aM>::DQVVXX&&&"4(((OO13R53R3RSSSJJt	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 !!##	JJt}dn555c"""		   

		   

 sU   .A:F44F8;F85I 
H I ?I I>II>I;8I>:I;;I>c                 r   K   | j                             |i           }|d||                    dd          dS )z'Return basic info about the email chat.r  r  r~   )rN  r   r  r  )r(  r   )rS   r  rC  s      r'   get_chat_infozEmailAdapter.get_chat_info  sG      "&&w33wwy"--	
 
 	
r)   r:   N)NNr/   )NNN)Nr  )'rV   rW   rX   __doc__r   r  r6  r<  r=  rJ  r1   rC   rl  rb  rn  r   r   r   r   rs  staticmethodr  r  rt  r   r   r  r  r  r  r  r   floatr  r  r  r   r  __classcell__)r*  s   @r'   r	  r	    s       EE>I~ >I >I >I >I >I >I@U U U U((,w| (, (, (, (,T 5: B B BT Bd B B B BH
- 
- 
- 
-	5 	5 	5 	53 3 3 3`T$sCx.%9 ` ` ` `D 
 
 
 
 \
 
$ 
 
 
 \
q)S#X q)4 q) q) q) q)n #'-1; ;; ; 3-	;
 4S>*; 
; ; ; ;$C     *.	) )) ) "#	)
 
) ) ) )V7 7 7c3h8P 7\` 7 7 7 7 "&"&-1@ @@ @ #	@
 3-@ 4S>*@ 
@ @ @ @* .2 2W 2W2W U38_%2W 4S>*	2W
 2W 
2W 2W 2W 2W 2W 2Wh44 4 I	4
 
4 4 4 4t "&#'"&; ;; ; #	;
 C=; 3-; 
; ; ; ;< $(2 22 2 	2
 C=2 
2 2 2 2h
3 
4S> 
 
 
 
 
 
 
 
r)   r	  )	thread_idmedia_filesforce_documentc                  K   ddl }ddl}ddlm} ddlm}	 t          | di           pi }
|
                    d          pt          j	        dd          }t          j	        d	d          }|
                    d
          pt          j	        dd          }	 t          t          j	        dd                    }n# t          t          f$ r d}Y nw xY wt          |||g          sddiS 	  ||dd          }||d<   ||d<   d|d<    |	d          |d<    |j        ||          }|                    |                                           |                    ||           |                    |           |                                 dd|dS # t(          $ r;}	 ddlm}  |d|           cY d}~S # t(          $ r dd| icY cY d}~S w xY wd}~ww xY w)zOut-of-process Email delivery via SMTP (one-shot). Implements the
    standalone_sender_fn contract; replaces the legacy _send_email helper.r   Nr   r   r  rv   r   r~   r   r  r   r  587r  rU  zNEmail not configured (EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_SMTP_HOST required)r  r   r{  r  r  r|  Tr  r  r;  email)r  platformr  )_errorzEmail send failed: )r<  rE  email.mime.textr   email.utilsr	   ra   r   r   r   r-  r  r3  r   r=  r@  rF  rY  r  r^  ro   tools.send_message_toolr  )pconfigr  re  r  r  r  r<  _sslr   r	   r  rv   passwordr  	smtp_portr   serverrt   _es                      r'   _standalone_sendr    s      NNN((((((&&&&&&GWb))/REii	""Dbi&D&DGy)2..H		+&&J")4Er*J*JI	"3U;;<<			"   			 9-.. kijj8hw11FD	'I j4000Fi33 ; ; = =>>>Wh'''C   WIII 8 8 8	8<<<<<<2/A//00000000 	8 	8 	86166777777777	8	8sO   "B7 7CC'B(F 
GF4.G4GGGGGGc                     t          | di           pi }|                    d          rdS ddlm} t	          |                    d          pd                                          S )zEmail is connected when an address is configured (in PlatformConfig.extra
    or via EMAIL_ADDRESS). Mirrors the legacy
    _PLATFORM_CONNECTED_CHECKERS[Platform.EMAIL] = bool(extra.get('address')).r  rv   Tr   Nr   r~   )ra   r   hermes_cli.gatewaygatewayr1   get_env_valuer   )r
  r  gateway_mods      r'   _is_connectedr#    sr     FGR((.BEyy t,,,,,,**?;;ArHHJJKKKr)   c                      t          |           S )zCFactory wrapper that constructs EmailAdapter from a PlatformConfig.)r	  )r
  s    r'   _build_adapterr%    s    r)   c                 z    |                      ddt          t          t          g dddddt          dd	d
d	           dS )u:   Plugin entry point — called by the Hermes plugin system.r  Email)r   r   r   u@   Email uses the Python stdlib (smtplib/imaplib) — no extra depsr  r  EMAIL_HOME_ADDRESSr4   Tu   📧)rN  labeladapter_factorycheck_fnis_connectedrequired_envinstall_hintallowed_users_envallow_all_envcron_deliver_env_varstandalone_sender_fnmax_message_lengthpii_safeemojiallow_update_commandN)register_platformr%  r   r#  r  )rC  s    r'   registerr8    s\    &)"KKKW/-1-!!      r)   r/   )rh   ri   r:   N)Fr  )Tr  r`  r  r  rW  loggingr   r   r<  r=   rE  r  email.headerr   email.mime.multipartr   r  r   email.mime.baser   r  r	   r
   pathlibr   typingr   r   r   r   r   gateway.platforms.baser   r   r   r   r   r   gateway.configr   r   utilsr   r   	getLoggerrV   rq   r   r   MAX_MESSAGE_LENGTHr?  r   r-  r
  rN   r=  rP   r>  r[   r  ru   r  r1   r   r   r   re  Messager   r   r   r   r   compiler   r   r   r   r  r	  r  r#  r%  r8  rY   r)   r'   <module>rF     s]   "        				 				   



  & & & & & & . . . . . . $ $ $ $ $ $ $ $ $ $ $ $ " " " " " "             3 3 3 3 3 3 3 3 3 3 3 3 3 3                4 3 3 3 3 3 3 3 # # # # # # # #		8	$	$  21AA 1 1))	      	> >
>
> > 	>
 ]> > > >>
 
 
 
 
 
 
 

 
 
 
 
G$ 
 
 
 988D D D D.	# 	 	 	 	 	 	
($ 
( 
( 
( 
(	c 	c 	 	 	 	#I-5 ## # # # #Lc c         " " " " " "6 6 6 6 6 6 6& "*("- 
 
YM  	M= M= M=			"M=M= 	M=
 49M= M= M= M=d #9 9			"99 
$sCx.9 9 9 9xq
 q
 q
 q
 q
& q
 q
 q
J .8 .8 .8 .8 .8bLT L L L L     
     r)   