
    epjp                        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
m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 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/  ej0        e1          Z2dKde3de3de3fdZ4e4Z5de3de6de6fdZ7dLde3de8de8fdZ9dZ:d d d d dZ;dZ<dZ=	 dMd e3d!e6d"e>d#ede	j	        f
d$Z? G d% d&ej@                  ZA G d' d(ejB                  ZCh d)ZDdNd,ZEd-e3d.eFde8fd/ZGde8fd0ZHd1e3de3fd2ZId3ejJ        jK        de3fd4ZLd5e3de3fd6ZMd1e3de3fd7ZNd-e3de3fd8ZOd9e3d:e3de8fd;ZP ejQ        d<ejR                  ZS ejQ        d=ejR                  ZTdd>d3ejJ        jK        d?e3d@e3de#e8e3f         fdAZU	 dLd3ejJ        jK        dBe8de!e e3ef                  fdCZV G dD dEe%          ZWddddFdGZXde8fdHZYdI ZZdOdJZ[dS )Pu  
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)UnscopedSecretError)
get_secret)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)is_truthy_value namedefaultreturnc                 z    	 t          | |          }n$# t          $ r t          j        |           }Y nw xY w||n|S )ug  Scope-aware ``EMAIL_*`` read with the default-profile startup fallback.

    Secondary profiles run under ``_profile_runtime_scope`` — the scope is
    authoritative and a scoped miss returns ``default`` (no cross-profile
    borrow). The DEFAULT profile's adapter constructs and sends *unscoped*
    under multiplexing, where a bare ``get_secret`` would raise
    ``UnscopedSecretError`` and crash its email path; there ``os.environ``
    is that profile's own value, so fall back to it. Same pattern as the
    Slack ``SLACK_APP_TOKEN`` read (#59739) and the WhatsApp
    ``_get_wsecret`` fix (5438e9c629).
    )_scoped_get_secret_UnscopedSecretErrorosgetenv)r   r   vals      E/home/thesage/.hermes/hermes-agent/plugins/platforms/email/adapter.py_get_esecretr'   7   sR     w//   ioo/33w.s    44c                     t          t          | d                                                    }|s|S 	 t          |          S # t          t
          f$ r |cY S w xY w)zCScope-aware integer read (``env_int`` variant of ``_get_esecret``).r   )strr'   stripint
ValueError	TypeError)r   r   raws      r&   _esecret_intr/   N   sk    
l4$$
%
%
+
+
-
-C 3xx	"   s   A AAFc                 @    t          t          | d          |          S )zDScope-aware boolean read (``env_bool`` variant of ``_get_esecret``).r   )r   )r   r'   )r   r   s     r&   _esecret_boolr1   Y   s    <b117CCCC    )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    r&   <lambda>r?   g   s    		T 1 r2   c                 .    |                                  dv S )N>   bulkjunklistr;   r=   s    r&   r?   r?   h   s    AGGII)AA r2   c                      t          |           S Nboolr=   s    r&   r?   r?   i   s    $q'' r2   c                      t          |           S rE   rF   r=   s    r&   r?   r?   j   s    $q'' r2   )zAuto-Submitted
PrecedencezX-Auto-Response-SuppresszList-UnsubscribeP     hostporttimeoutsource_addressc                    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)rL   rM   rN   rO   
last_errorfamilysocktypeproto
_canonnamesockaddrsockexcs               r&   _create_ipv4_connectionrc   s   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rO   )rc   rO   )selfrL   rM   rN   s       r&   _get_socketz_IPv4SMTP._get_socket   s(    &.	
 
 
 	
r2   N__name__
__module____qualname__ri    r2   r&   re   re      s#        
 
 
 
 
r2   re   c                       e Zd Zd ZdS )_IPv4SMTP_SSLc                     t          |||| j                  }| j                            |t	          | d|                    S )Nrg   _host)server_hostname)rc   rO   contextwrap_socketgetattr)rh   rL   rM   rN   raw_socks        r&   ri   z_IPv4SMTP_SSL._get_socket   sX    *.	
 
 
 |''#D'488 ( 
 
 	
r2   Nrj   rn   r2   r&   rp   rp      s#        

 

 

 

 

r2   rp   >   .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_clir   	Exceptionxatomloggerdebug)r}   _hermes_versiones      r&   _send_imap_idr      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 rE   rn   ).0patternaddrs     r&   	<genexpr>z'_is_automated_sender.<locals>.<genexpr>   s'      
<
<w7d?
<
<
<
<
<
<r2   Tr   F)r<   any_NOREPLY_PATTERNS_AUTOMATED_HEADERSitemsget)r   r   headercheckvaluer   s        @r&   _is_automated_senderr      s    ==??D

<
<
<
<*;
<
<
<<< t+1133  FB'' 	UU5\\ 	445r2   c                  8   t          dd                                          } t          dd                                          }t          dd                                          }t          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)_get_secretr*   all)r   pwdr}   smtps       r&   check_email_requirementsr      s     ++1133D
&
+
+
1
1
3
3C("--3355D("--3355Dc4&'''r2   r.   c                    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r2   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_typer)   r   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rr2   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::<<r2   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;;r2   c                     |                      d          \  }}}|                                                                S )z=Return the lowercased domain part of an email address, or ''.@)
rpartitionr*   r<   )r   _domains      r&   
_domain_ofr   #  s5    %%c**LAq&<<>>!!!r2   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5r2   z!\b(dmarc|dkim|spf)\s*=\s*([a-z]+)zR\b(header\.from|header\.d|smtp\.mailfrom|smtp\.from|envelope-from)\s*=\s*([^\s;]+)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 rn   r;   )r   mrs      r&   
<dictcomp>z1_verify_sender_authentication.<locals>.<dictcomp>{  s.    QQQ1qwwyy!''))QQQr2   c                     i | ]?\  }}|                                 |                                                    d           @S )")r<   r*   )r   pr>   s      r&   r   z1_verify_sender_authentication.<locals>.<dictcomp>|  s:    XXXAQWWYY		,,XXXr2   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_domainr   trustedr.   r   servmethodsprops
spf_domaindkim_domains                r&   _verify_sender_authenticationr  G  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<<<<<r2   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   r,   r   r   r   r   )
r   r  attachmentsr   r   r   r  extr   cached_paths
             r&   _extract_attachmentsr    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                 h   t                                          |t          j                   |j        pi }t          dd          p|                    dd                                          | _        t          dd          | _	        t          dd          p|                    dd                                          | _
        t          dd          | _        t          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          dd                                                                          | _        t-                      | _        d| _        d | _        i | _        t6                              d| j                   d S )Nr   r   r   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*   _address	_password
_imap_hostr/   
_imap_port
_smtp_host
_smtp_port_poll_interval_skip_attachmentsrG   _require_authenticated_senderr1   r<   _authserv_idset
_seen_uids_seen_uids_max
_poll_task_thread_contextr   info)rh   r  r(  	__class__s      r&   r&  zEmailAdapter.__init__  s   000 "$_b99UUYYyRT=U=U\\^^$%5r::&'8"==[;XZA[A[bbdd&'8#>>&'8"==[;XZA[A[bbdd&'8#>>*+@"EE "'+=u!E!E  *U2215e<Z6[1\1\D..4e<< 	616D..15D. IImR((PK8KR,P,P
%''%%'' 	
  #uu#'26 ;=8$-HHHHHr2   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 rE   )r+   )us    r&   r?   z.EmailAdapter._trim_seen_uids.<locals>.<lambda>"  s    A r2   )key   z'[Email] Trimmed seen UIDs to %d entries)
lenr4  r5  sortedr3  r   r   r,   r-   rC   )rh   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_onlyrE  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  )rN   rt   rN   rt   )	re   smtplibSMTPrp   SMTP_SSLSMTP_CONNECT_TIMEOUTstarttlsr   rZ   )rE  smtp_clssmtp_ssl_clsr   ctxrL   rM   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.  rG   rI  rJ  rR   rN   TimeoutErrorConnectionErrorrY   r   SSLError)rh   rQ  rb   rP  rL   rM   s      @@@r&   _connect_smtpzEmailAdapter._connect_smtp*  s     (***/ 	 	 	4 	GL 	 	 	 	 	 	 	 		,8::owG 	, 	, 	,#s|,,  8d+++++++++	,s   	A "B!0&BB!B!F)is_reconnectrX  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 rn   rn   )r   r   r   s      r&   
<listcomp>z(EmailAdapter.connect.<locals>.<listcomp>Y  s4     	
 	
 	
e 	
	
 	
 	
r2   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)	retryablerK   rG  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   r   error_set_fatal_errorimaplib	IMAP4_SSLr,  loginr   selectuidr   r4  addrC  logoutr8  r?  r   rW  quit_runningasynciocreate_task
_poll_loopr6  print)
rh   rX  missingmessager}   statusdatarg  r   r   s
             r&   rX   zEmailAdapter.connectT  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.)rk  r6  cancelrl  CancelledErrorr   r8  rh   s    r&   
disconnectzEmailAdapter.disconnect  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)	rk  _check_inboxrl  rv  r   r   ra  sleepr/  )rh   r   s     r&   rn  zEmailAdapter._poll_loop  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)rl  get_running_looprun_in_executor_fetch_new_messages_dispatch_message)rh   loopmessagesmsg_datas       r&   rz  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 	3r2   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.rK   rG  r^  r   NUNSEENr`  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)rg  sender_addrsender_namesubject
message_idin_reply_tobodyr  datesender_authenticatedauth_reasonz[Email] IMAP fetch error: %s)*rc  rd  r+  r,  re  r)  r*  r   rf  rg  ri  r   r   r4  rh  r?  r5  rC  
IndexErrorr-   r   warningr   r   	bytearray	email_libmessage_from_bytesr   r   r   r*   dictr   r   r   r  r2  r   r  r0  r   ra  )rh   resultsr}   rr  rs  rg  r  	raw_emailr   
sender_rawr  r  r  r  r  msg_headersr  r  r  r  r   s                        r&   r  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          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#   r$   )truthys    r&   _allow_all_senderszEmailAdapter._allow_all_senders  sr     &%%/44::<<BBDDN Ry2B77==??EEGG6Q	
r2   c                      t          t          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)rG   r   r*   r#   r$   rn   r2   r&   _allowlist_in_effectz!EmailAdapter._allowlist_in_effect(  sQ     -r2288:: >y0"55;;==
 
 	
r2   r  c           
        K   |d         }|| j                                         k    rdS t          |i           rt                              d|           dS t          dd                                          }|st          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 rn   )r*   r<   )r   r   s     r&   	<setcomp>z1EmailAdapter._dispatch_message.<locals>.<setcomp>U  s>    ___RVR\R\R^R^_tzz||))++___r2   ,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   r   r   r   r*   r#   r$   r   r1  r  r  r   r  
startswithr   TEXTr   PHOTODOCUMENTr7  build_sourcer   r8  handle_message)rh   r  r  allowed_rawallowedr  r  r  r   r  r  msg_typeattr  events                  r&   r  zEmailAdapter._dispatch_message7  s     }- $---////F  R00 	LLLkZZZF ""7<<BBDD 	2B77==??EEGGOccc	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!!%(((((((((((r2   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  ra  )	rl  r}  r~  _send_emailr   r   r   ra  r)   )rh   r  r  r  r  r  r  r   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)  rsplitrw  s    r&   _message_id_domainzEmailAdapter._message_id_domain  s8     $-=''Q//3B{B{r2   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)  r7  r   r  r   uuiduuid4hexr  attachr   rW  re  r*  send_messagerj  r   rZ   r   r8  )
rh   r  r  r  r   rP  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.Nrn   )rh   r  r  s      r&   send_typingzEmailAdapter.send_typing  s
        r2   	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*   )rh   r  r  r  r  r  r   s          r&   
send_imagezEmailAdapter.send_image  sT       }")i)))YYw

h?????????r2           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   existsr   r  r   rl  r}  r~  _send_email_with_attachmentsr   ra  r%  send_multiple_images)rh   r  r  r  r  _unquote
body_partslocal_pathsr  alt_text
local_pathr  r  r   r9  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)  r7  r   r  r   r  r  r  r  r  r   r   openr	   set_payloadreadr   encode_base64
add_headerr   r   r   r  rW  re  r*  r  rj  rZ   r8  r?  )rh   r  r  r  r   rP  r  r  r  	file_pathr   fr   r   r   s                  r&   r  z)EmailAdapter._send_email_with_attachments9  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  )	rl  r}  r~  _send_email_with_attachmentr   r   r   ra  r)   )
rh   r  r  r  r
  r  kwargsr  r  r   s
             r&   send_documentzEmailAdapter.send_documento  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)  r7  r   r  r   r  r  r  r  r  r   r   r   r  r	   r  r  r   r  r  rW  re  r*  r  rj  r   rZ   )rh   r  r  r  r
  r   rP  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   )r   r  r  r  )r7  r   )rh   r  rP  s      r&   get_chat_infozEmailAdapter.get_chat_info  sG      "&&w33wwy"--	
 
 	
r2   r   N)NNrE   )NNN)Nr  )'rk   rl   rm   __doc__r   r&  rC  rI  rJ  rW  rG   rX   rx  rn  rz  r   r   r)   r   r  staticmethodr  r  r  r   r   r  r  r  r  r  r   floatr  r  r  r  r  __classcell__)r9  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> 
 
 
 
 
 
 
 
r2   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          dd          }t          d	d          }|
                    d
          pt          dd          }	 t          t          dd          p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(  r   r   r   r   r  r   r  587r  ra  zNEmail not configured (EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_SMTP_HOST required)r  r   r  r  r  r  Tr  r  rH  email)r  platformr  )_errorzEmail send failed: )rI  rR  email.mime.textr   email.utilsr   rv   r   r   r+   r,   r-   r   rJ  rM  rS  re  r  rj  r   tools.send_message_toolr  )pconfigr  rq  r  r  r  rI  _sslr   r   r(  r   passwordr  	smtp_portr   serverr   _es                      r&   _standalone_sendr)    s~      NNN((((((&&&&&&GWb))/REii	""Fk/2&F&FG+R00H		+&&L+6G*L*LI$5u==FGG			"   			 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   B% %B;:B;B(E> >
G	F"G"F;3F>4G:F;;F>>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(  r   Tr   Nr   r   )rv   r   hermes_cli.gatewaygatewayrG   get_env_valuer*   )r  r(  gateway_mods      r&   _is_connectedr/    sr     FGR((.BEyy t,,,,,,**?;;ArHHJJKKKr2   c                      t          |           S )zCFactory wrapper that constructs EmailAdapter from a PlatformConfig.)r  )r  s    r&   _build_adapterr1    s    r2   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_ADDRESSrJ   Tu   📧)r   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_platformr1  r   r/  r)  )rP  s    r&   registerrD    s\    &)"KKKW/-1-!!      r2   )r   )FrE   )r}   r~   r   Nr  )\r  rl  r  r  rc  loggingr#   r   rI  rR   agent.secret_scoper   r"   r   r!   rR  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   	getLoggerrk   r   r)   r'   r   r+   r/   rG   r1   r   r   MAX_MESSAGE_LENGTHrL  r  rc   rJ  re   rK  rp   r  r   r  r   r   r   rq  Messager   r   r   r   r   compiler   r   r   r  r  r  r)  r/  r1  rD  rn   r2   r&   <module>rS     s   "        				 				   K J J J J J ? ? ? ? ? ? 



  & & & & & & . . . . . . $ $ $ $ $ $ $ $ $ $ $ $ " " " " " "             3 3 3 3 3 3 3 3 3 3 3 3 3 3                4 3 3 3 3 3 3 3 ! ! ! ! ! !		8	$	$/ /s /S /# / / / /( s S S    D D Dd Dt D D D D  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     
     r2   