
    `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jX                  e-      Z.dZ/d d d d dZ0dZ1dZ2	 dBde3de4de5dede	j                  f
dZ6 G d dejn                        Z8 G d dejr                        Z:h d Z;dCd!Z<d"e3d#e=de>fd$Z?de>fd%Z@d&e3de3fd'ZAd(ej                  j                  de3fd)ZDd*e3de3fd+ZEd&e3de3fd,ZFd"e3de3fd-ZGd.e3d/e3de>fd0ZH ej                  d1ej                        ZK ej                  d2ej                        ZLd3d4d(ej                  j                  d5e3d6e3dee>e3f   fd7ZM	 dDd(ej                  j                  d9e>deee3ef      fd:ZN G d; d<e       ZOddd8d=d>ZPde>fd?ZQd@ ZRdEdAZSy)Fu  
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                 (    | j                         dk7  S )Nnolowervs    R/root/.hermes/venv/lib/python3.12/site-packages/plugins/platforms/email/adapter.py<lambda>r(   :   s    	T 1     c                 &    | j                         dv S )N>   bulkjunklistr#   r%   s    r'   r(   r(   ;   s    AGGI)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 ]X  \  }}}}}	t        j                   |||      }
|
j	                  |       	 |r|
j                  |       |
j                  |	       |
c S  ||t        d|  d|       # t        $ r}|}|
j                          Y d}~d}~ww xY w)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?9K9KdFNNF$6$6: 5%X }}VXu5 			.)LL"K 
.tfAdV<
==  	JJJLL	s   )%B''	C0CCc                       e Zd Zd Zy)	_IPv4SMTPc                 4    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y)_IPv4SMTP_SSLc                     t        |||| j                        }| 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   sK    *..	
 ||''#D'48 ( 
 	
r)   NrU   rY   r)   r'   r[   r[   o   s    

r)   r[   >   .gif.jpg.png.jpeg.webpc                     	 	 ddl m} | j                  dd| d       y# t        $ r d}Y $w xY w# t        $ r }t        j                  d|       Y d}~y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_cliri   	Exceptionxatomloggerdebug)imap_hermes_versiones      r'   _send_imap_idrt      so    D	"A 	

00A B: :	
  	"!O	"  D?CCDs(     1 .1 .1 	AAAaddressheadersc                     | j                         t        fdt        D              ryt        j	                         D ]$  \  }}|j                  |d      }|s ||      s$ y y)z>Return True if this email is from an automated/noreply source.c              3   &   K   | ]  }|v  
 y wr/   rY   ).0patternaddrs     r'   	<genexpr>z'_is_automated_sender.<locals>.<genexpr>   s     
<w7d?
<s   T F)r$   any_NOREPLY_PATTERNS_AUTOMATED_HEADERSitemsget)ru   rv   headercheckvaluer{   s        @r'   _is_automated_senderr      s]    ==?D

<*;
<<+113 FB'U5\ r)   c                  @   t        j                  dd      j                         } t        j                  dd      j                         }t        j                  dd      j                         }t        j                  dd      j                         }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{   pwdrq   smtps       r'   check_email_requirementsr      s     99_b)//1D
))$b
)
/
/
1C99&+113D99&+113Dc4&''r)   rawc                     t        |       }g }|D ]M  \  }}t        |t              r'|j                  |j	                  |xs dd             =|j                  |       O dj                  |      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      si    #EG !gdE"NN4;;w'9');LMNN4 	!
 88Gr)   msgc                    | j                         r| j                         D ]u  }|j                         }t        |j	                  dd            }d|v r3|dk(  s9|j                  d      }|sN|j                         xs d}|j                  |d	      c S  | j                         D ]  }|j                         }t        |j	                  dd            }d|v r3|d
k(  s9|j                  d      }|sN|j                         xs d}|j                  |d	      }t        |      c S  y| j                  d      }|rG| j                         xs d}|j                  |d	      }| j                         d
k(  rt        |      S |S y)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   
HHJ 
	ED002Ldhh'<bABK{*|+**$*7"668CGG">>')>DD
	E HHJ 
	-D002Ldhh'<bABK{*{***$*7"668CGG">>')>DD&t,,
	- ///.--/:7G>>')><D##%4"4((Kr)   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|      }|j                         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    66,d"--@D66+tT?D66'4R]];D66*b$'D66)S$'D66(C&D66'3%D66'3%D66)VT*D::<r)   c                     t        j                  d|       }|r-|j                  d      j                         j	                         S | j                         j	                         S )z5Extract bare email address from 'Name <addr>' format.z	<([^>]+)>   )r   searchgroupr   r$   )r   matchs     r'   _extract_email_addressr      sJ    IIlC(E{{1~##%++--99;r)   c                 h    | j                  d      \  }}}|j                         j                         S )z=Return the lowercased domain part of an email address, or ''.@)
rpartitionr   r$   )ru   _domains      r'   
_domain_ofr      s-    %%c*LAq&<<>!!r)   abc                 0   | xs dj                         j                         j                  d      } |xs dj                         j                         j                  d      }| r|sy| |k(  ry| j                  d|z         xs |j                  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Av::cAg5!**S1W"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                r   t        |      }|sy| j                  d      xs g }|syd}|D ]  }dj                  t        |      j	                               }|rQ|j	                  dd      d   j                         j                         }t        ||      s||j                         k7  r~|} n |y	t        j                  |      D 	
ci c]$  \  }	}
|	j                         |
j                         & }}	}
t        j                  |      D ci c]3  \  }}|j                         |j                         j                  d
      5 }}}|j                  d      dk(  ry|j                  d      dk(  rat        |j                  dd            xs& |j                  dd      xs |j                  dd      }d|v rt        |      n|}t        ||      ry|j                  d      dk(  r<|j                  dd      xs t        |j                  dd            }t        ||      rydd|dd  dfS c c}
}	w c c}}w )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-id"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_domainrv   trustedr   r   servmrmethodspr&   props
spf_domaindkim_domains                    r'   _verify_sender_authenticationr     s   6 Y'K+ kk239rG8G S)*;;sA&q)//1779D#D+64;CTCTCV;V J0?0G0G0PQ1qwwy!'')#QGQ9F9N9Nw9WXAQWWY	,,XEX {{7v%! {{5V#		/2 >? ,599D
 ,YY+ 	 03j/@Z
+j
J4+ {{6f$ii
B/[:eiiWY>Z3[K5,+GDSM?!<<<3 RXs   	)H-8H3Fskip_attachmentsc                    g }| j                         s|S | j                         D ]  }t        |j                  dd            }|r	d|v sd|v r*d|vrd|vr3|j	                         }|dv rd|vrL|j                         }|rt        |      }n|j                         xs d}d| }|j                  d	      }|st        |      j                  j                         }|t        v r$	 t        ||      }	|j#                  |	|d|d       t%        ||      }	|j#                  |	|d|d        |S # t        $ r t        j!                  d
|       Y 0w xY w)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   
ValueErrorro   rp   r   r   )
r   r   attachmentsr   r   r   r   extr   cached_paths
             r'   _extract_attachmentsr  j  s    K
 *$((#8"=>!<K@W{*x{/J,,.66<{;Z$$&+H5H**,5C$SE*H""$"/8n##))++4WcB #$*	   4GXFK#$"*	  K*X %  UW_`s   )D33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	 	 	 d/deded ee   dee   deeeef      defd!Z	 	 d0d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"	 	 	 d/d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 )1EmailAdapterz;Email gateway adapter using IMAP (receive) and SMTP (send).configc                    t         |   |t        j                         |j                  xs i }t        j                  dd      xs |j                  dd      j                         | _	        t        j                  dd      | _
        t        j                  dd      xs |j                  dd      j                         | _        t        dd      | _        t        j                  d	d      xs |j                  d
d      j                         | _        t        dd      | _        t        dd      | _        |j                  dd      | _        d|v rt%        |d         | _        nt)        dd      rd| _        nd| _        |j                  dd      xs t        j                  dd      j                         j+                         | _        t/               | _        d| _        d | _        i | _        t8        j;                  d| j                         y )Nr   r}   ru   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_contextro   info)rS   r  r  	__class__s      r'   r  zEmailAdapter.__init__  s   0 "?B7S599YPR;SZZ\#3R899%6;YuyyVX?Y``b!"3S999%6;YuyyVX?Y``b!"3S9%&;R@ "'+=u!E  *U215e<Z6[1\D./716D.15D. IImR(NBII6I2,N
%'%%' 	
  #u#'26 ;=8$--Hr)   r:   Nc                    t        | j                        | j                  k  ry	 t        | j                  d       }| j                  dz  }t	        || d       | _        t
        j                  dt        | j                               y# t        t        f$ r7 t	        t        | j                        | j                   dz  d       | _        Y y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   ro   rp   r   	TypeErrorr-   )rS   sorted_uidskeeps      r'   _trim_seen_uidszEmailAdapter._trim_seen_uids  s     t4#6#66	U 6FGK&&!+D!+tef"56DOLLBCDXYI& 	U!$t"79L9L8LPQ8Q8R"STDO	Us   A$B
 
ACCc                 Z   t        j                         | j                  | j                  dddt        dt
        j                  ffd}	  |       S # t        j                  t        t        t        f$ r.}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_onlyr4  r:   c                    | rt         nt        j                  }| rt        nt        j                  }dk(  r |t
              S  |t
              }	 |j                         |S # t        $ r |j                           w xY w)zAttempt one SMTP connection.i  )r8   r_   r8   r_   )	rP   smtplibSMTPr[   SMTP_SSLSMTP_CONNECT_TIMEOUTstarttlsrm   rE   )r4  smtp_clssmtp_ssl_clsr   ctxr6   r7   s       r'   _connectz,EmailAdapter._connect_smtp.<locals>._connect  s    $-y7<<H,5=7;K;KLs{#D$8LVYZZD$0DEDc* K  

s   A+ +BTN)sslcreate_default_contextr  r  r1   r8  r9  r=   r8   TimeoutErrorConnectionErrorrD   r   SSLError)rS   r@  rM   r?  r6   r7   s      @@@r'   _connect_smtpzEmailAdapter._connect_smtp  s     ((**/ 	4 	GLL 		,:owG 	,#s||, d++	,s   A #B*<#B%B*%B*F)is_reconnectrG  c                p  K   d| j                   fd| j                  fd| j                  fd| j                  ffD cg c]	  \  }}|s| }}}|rBddj	                  |      z   dz   }t
        j                  d|       | j                  d	|d
       y
	 t        j                  | j                  | j                  d      }|j                  | j                   | j                         t        |       |j                  d       |j                  ddd      \  }}|dk(  r:|r8|d   r3|d   j                         D ]  }	| j                   j#                  |	        | j%                          |j'                          t
        j)                  dt+        | j                                	 | j/                         }	 |j                  | j                   | j                         |j1                          t
        j)                  d       d| _        t5        j6                  | j9                               | _        t=        d| j                           yc c}}w # t,        $ r }
t
        j                  d|
       Y d}
~
y
d}
~
ww xY w# |j1                          w xY w# t,        $ r }
t
        j                  d|
       Y d}
~
y
d}
~
ww xY ww)z>Connect to the IMAP server and start polling for new messages.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   r6  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   ro   error_set_fatal_errorimaplib	IMAP4_SSLr  loginrt   selectuidr   r!  addr2  logoutr%  r-  rm   rF  quit_runningasynciocreate_task
_poll_loopr#  print)rS   rG  namer   missingmessagerq   statusdatarT  rs   r   s               r'   rC   zEmailAdapter.connect'  s[     !$--0!4>>2"DOO4"DOO4	 	
e  	
 	
 -))G$%""  LLw/
 !!-w% "  	$$T__doorRDJJt}}dnn5$KK 88HdE:LFD~$477==? -COO'',-   "KKMKK\^abfbqbq^rs

	%%'D

4==$..9		KK>?
 !--doo.?@%dmm_56{	
T  	LL=qA	 		 	LL=qA	sy   :J6I
AJ6DI	 J
 0&I5 %J
 ;AJ6		I2I-(J6-I22J65JJ
 
	J3J.)J6.J33J6c                    K   d| _         | j                  r6| j                  j                          	 | j                   d{    d| _        t
        j                  d       y7 !# t        j                  $ r Y 4w xY ww)zStop polling and disconnect.FNz[Email] Disconnected.)rX  r#  cancelrY  CancelledErrorro   r%  rS   s    r'   
disconnectzEmailAdapter.disconnectk  sh     ??OO""$oo%% #DO+,	 &)) s:   .A=A$  A"A$ A="A$ $A:7A=9A::A=c                 H  K   | j                   rN	 | j                          d{    t        j                  | j                         d{    | j                   rMyy7 :# t        j                  $ r Y yt        $ r }t
        j                  d|       Y d}~pd}~ww xY w7 Uw)z0Poll IMAP for new messages at regular intervals.Nz[Email] Poll error: %s)	rX  _check_inboxrY  rd  rm   ro   rN  sleepr  )rS   rs   s     r'   r[  zEmailAdapter._poll_loopw  s     mm:'')))
 -- 3 3444 mm)))  :5q99:4s[   B"A  AA  "B"
B B"B"A   B3B"5B=BB"BB"c                    K   t        j                         }|j                  d| j                         d{   }|D ]  }| j	                  |       d{     y7 %7 	w)z2Check INBOX for unseen messages and dispatch them.N)rY  get_running_looprun_in_executor_fetch_new_messages_dispatch_message)rS   loopmessagesmsg_datas       r'   rh  zEmailAdapter._check_inbox  s^      '')--dD4L4LMM  	3H((222	3 N2s!   4A AA AA A c                    g }	 t        j                  | j                  | j                  d      }	 |j	                  | j
                  | j                         t        |       |j                  d       |j                  ddd      \  }}|dk7  s|r|d   s|	 |j                          S |d   j                         D ]	  }|| j                  v r| j                  j                  |       t        | j                        | j                   kD  r| j#                          |j                  d	|d
      \  }}|dk7  r|	 |d   d   }t-        |t.        t0        f      st(        j+                  d|       t3        j4                  |      }|j7                  dd      }	t9        |	      }
t;        |	      }d|v r1|j                  d      d   j=                         j=                  d      }t;        |j7                  dd            }|j7                  dd      }|j7                  dd      }t?        |jA                               }tC        |
|      rt(        jE                  d|
       tG        ||
| jH                        \  }}tK        |      }tM        || jN                        }|jQ                  ||
|||||||j7                  dd      ||d        	 	 |j                          |S # t        $ r Y S w xY w# t$        t&        f$ r t(        j+                  d|       Y Xw xY w# t        $ r Y |S w xY w# 	 |j                          w # t        $ r Y w w xY wxY w# t        $ r!}t(        jS                  d|       Y d}~|S d}~ww xY w)z?Fetch new (unseen) messages from IMAP. Runs in executor thread.r5   r6  rK  r   NUNSEENrM  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)rT  sender_addrsender_namesubject
message_idin_reply_tobodyr   datesender_authenticatedauth_reasonz[Email] IMAP fetch error: %s)*rP  rQ  r  r  rR  r  r  rt   rS  rT  rV  rm   r   r!  rU  r-  r"  r2  
IndexErrorr/  ro   warningr   r   	bytearray	email_libmessage_from_bytesr   r   r   r   dictr   r   rp   r   r  r   r  r  r   rN  )rS   resultsrq   r`  ra  rT  rq  	raw_emailr   
sender_rawrz  r{  r|  r}  r~  msg_headersr  r  r  r   rs   s                        r'   rm  z EmailAdapter._fetch_new_messages  sG   \	<$$T__doorRDX

4==$..9d#G$#xx$AT>T!W"\KKM[  7==? JCdoo- OO'',4??+d.A.AA,,.'+xxj'I$FH~ !$,QKN	 &i%1CDQSV !#66yAC!$!4J"8"DK"6z"BKk)&1&7&7&<Q&?&E&E&G&M&Mc&R23779n3UVG!$r!:J"%''-"<K"&syy{"3K+KE%LkZ  9V[d6G6G95(+ .c2D"6sTMcMc"dKNN"'2'2#*&0'2 $'2 # 30D'2$ }JXKKM
 	 ! u '	2 !] !!t !  	KKM   	<LL7;;	<s   ,M A%L' K(BL' 8K+ FL' 
L 	K(%M 'K((M +%LL' LL' 	L$ M #L$$M 'M	)L:9M	:	MM	MM		M 	M6M11M6c                      h d} t        j                  dd      j                         j                         | v xs4 t        j                  dd      j                         j                         | 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  s`     &II-r288:@@BfL Ryy2B7==?EEG6Q	
r)   c                      t        t        j                  dd      j                         xs$ t        j                  dd      j                               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  sF     II+R0668 >yy0"5;;=
 	
r)   rq  c           
      :  K   |d   }|| j                   j                         k(  ryt        |i       rt        j	                  d|       yt        j                  dd      j                         }|st        j                  dd      j                         j                         dvrt        j                  dd      j                         j                         dvrt        j	                  d	|       y|j                  d
      D ch c]1  }|j                         s|j                         j                         3 }}|j                         |vrt        j	                  d|       y| j                  rZ| j                         rJ| j                         s:|j                  dd      s(t        j                  d||j                  dd             y|d   }|d   j                         }|d   }|}	|r|j                  d      sd| d| }	g }
g }t        j                   }|D ]o  }|
j#                  |d          |j#                  |d          |d   dk(  r$|t        j                   k(  rt        j$                  }W|d   dk(  s`t        j&                  }q ||d   d| j(                  |<   | j+                  ||d   xs |d||d   xs |       }t-        |	xs d!|||d   |
||d"   xs d#      }t        j/                  d$||       | j1                  |       d{    yc c}w 7 
w)%z<Convert a fetched email into a MessageEvent and dispatch it.rz  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: %s,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   ro   rp   r   r   r   r   r  r  r  r   r  
startswithr   TEXTr   PHOTODOCUMENTr$  build_sourcer   r%  handle_message)rS   rq  rz  allowed_rawr{   allowedr|  r  r   r   r  r  msg_typeattr  events                   r'   rn  zEmailAdapter._dispatch_message
  s!    }- $----//  R0LLLkZ ii 5r:@@Byy0"5;;=CCEMaa		3R8>>@FFHPdd:
 8C8I8I#8N_RVR\R\R^tzz|))+_G_  "'1VXcd ..))+++-LL!7?NND ]L9 9%%%'}- 7--e4ydV4D 
## 	0Cc&k*s<016{g%(k6F6F*F&,,V
* '//	0 "<0-
[)
 ""}-<}-< # 
 (!-!# ( 7 ?4
 	5{GL!!%(((g `f 	)s,   C?LL L7E L8BLLLr  contentreply_tometadatac                 &  K   	 t        j                         }|j                  d| j                  |||       d{   }t	        d|      S 7 # t
        $ r7}t        j                  d||       t	        dt        |            cY d}~S d}~ww xY ww)z)Send an email reply to the given address.NTsuccessr}  z[Email] Send failed to %s: %sFr  rN  )	rY  rk  rl  _send_emailr   rm   ro   rN  r   )rS   r  r  r  r  ro  r}  rs   s           r'   sendzEmailAdapter.send}  s     	;++-D#33d&&(  J dzBB  	;LL8'1Ee3q6::	;sD   B6A AA BA 	B,B	BB	BBc                 f    d| j                   v r#| j                   j                  dd      d   xs dS y)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  rsplitre  s    r'   _message_id_domainzEmailAdapter._message_id_domain  s4     $--==''Q/3B{Br)   to_addrr  reply_to_msg_idc                 j   t               }| j                  |d<   ||d<   | j                  j                  |i       }|j                  dd      }|j	                  d      sd| }||d<   |xs |j                  d      }|r
||d	<   ||d
<   t        d      |d<   dt        j                         j                  dd  d| j                          d}||d<   |j                  t        |dd             | j                         }		 |	j                  | j                  | j                         |	j                  |       	 |	j!                          t&        j)                  d||       |S # t"        $ r |	j%                          Y 4w xY w# 	 |	j!                          w # t"        $ r |	j%                          Y w w xY wxY w)z0Send an email via SMTP. Runs in executor thread.ru  Tor|  Hermes Agentr  Re: rv  r}  rx  
ReferencesT	localtimery  <hermes-N   r   r   rw  plainr   z&[Email] Sent reply to %s (subject: %s))r   r  r$  r   r  r	   uuiduuid4hexr  attachr   rF  rR  r  send_messagerW  rm   rE   ro   r%  )
rS   r  r  r  r   r?  r|  original_msg_idmsg_idr   s
             r'   r  zEmailAdapter._send_email  s    ommFD	 ""&&w3'')^4!!%(WI&G I *BSWW\-B!0C /C 40FDJJL,,Sb12!D4K4K4M3NaP"L

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

		 

sB    7F  8E! !E=<E= F2FF2F/,F2.F//F2c                    K   yw)u(   Email has no typing indicator — no-op.NrY   )rS   r  r  s      r'   send_typingzEmailAdapter.send_typing  s     s   	image_urlcaptionc                 |   K   |xs d}|d| z  }| j                  ||j                         |       d{   S 7 w)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  sA      }"+i[))YYw

h????s   3<:<imageshuman_delayc                   K   |syddl m} g }g }|D ]  \  }}	|	r|j                  |	       |j                  d      rM ||dd       }
t	        |
      j                         r|j                  |
       `t        j                  d|
       w|j                  d|         |s|sydj                  |      }	 t        j                         }|j                  d| j                  |||       d{    y7 # t        $ r=}t        j                  d	|d
       t        | A  ||||       d{  7   Y d}~yd}~ww xY w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   existsro   r  r   rY  rk  rl  _send_email_with_attachmentsrm   rN  r  send_multiple_images)rS   r  r  r  r  _unquote
body_partslocal_pathsr  alt_text
local_pathr  ro  rs   r&  s                 r'   r  z!EmailAdapter.send_multiple_images  s=     4 "
!##) 	9Ix!!(+##I.%im4

#**,&&z2NN#GT !!GI;"78	9 :{{:&	W++-D&&11    	WLLLaZ^L_'.w+VVV	WsN   B3D>76C5 -C3.C5 2D>3C5 5	D;>-D6+D.,D61D>6D;;D>
file_pathsc                    t               }| j                  |d<   ||d<   | j                  j                  |i       }|j                  dd      }|j	                  d      sd| }||d<   |j                  d      }|r
||d	<   ||d
<   t        d      |d<   dt        j                         j                  dd  d| j                          d}||d<   |r|j                  t        |dd             |D ]  }	t        |	      }
	 t        |
d      5 }t        dd      }|j                  |j!                                t#        j$                  |       |j'                  dd|
j(                          |j                  |       ddd        | j1                         }	 |j3                  | j                  | j4                         |j7                  |       	 |j9                          t,        j=                  d|t?        |             |S # 1 sw Y   xY w# t*        $ r"}t,        j/                  d|	|       Y d}~Cd}~ww xY w# t*        $ r |j;                          Y ww xY w# 	 |j9                          w # t*        $ r |j;                          Y w w xY wxY w)z6Send an email with multiple file attachments via SMTP.ru  r  r|  r  r  r  rv  r}  rx  r  Tr  ry  r  Nr  r   r   rw  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_headerr]  rm   ro   r  rF  rR  r  r  rW  rE   r%  r-  )rS   r  r  r  r   r?  r|  r  r  	file_pathr   fr   rs   r   s                  r'   r  z)EmailAdapter._send_email_with_attachments  sY    ommFD	""&&w3'')^4!!%(WI&G I'',/!0C /C 40FDJJL,,Sb12!D4K4K4M3NaP"LJJxgw78# 
	PIYAP!T] %a#M>BD$$QVVX.**40OO$9=RSTSYSYRZ;[\JJt$%
	P !!#	JJt}}dnn5c"		 	JGUXYcUde)% %  P@)QOOP  

		 

ss   >H
A1H ;H7I H:  H		H	H7H22H7:IIJI,+J,JJJJr  	file_namec                 .  K   	 t        j                         }|j                  d| j                  ||xs d||       d{   }t	        d|      S 7 # t
        $ r6}	t        j                  d|	       t	        dt        |	            cY d}	~	S d}	~	ww xY ww)z#Send a file as an email attachment.Nr}   Tr  z [Email] Send document failed: %sFr  )	rY  rk  rl  _send_email_with_attachmentr   rm   ro   rN  r   )
rS   r  r  r  r  r  kwargsro  r}  rs   s
             r'   send_documentzEmailAdapter.send_documentB  s     	;++-D#33002  J dzBB  	;LL;Q?e3q6::	;sE   B;A A A BA 	B+BBBBBc                 ~   t               }| j                  |d<   ||d<   | j                  j                  |i       }|j                  dd      }|j	                  d      sd| }||d<   |j                  d      }|r
||d	<   ||d
<   t        d      |d<   dt        j                         j                  dd  d| j                          d}	|	|d<   |r|j                  t        |dd             t        |      }
|xs |
j                  }t        |
d      5 }t        dd      }|j!                  |j#                                t%        j&                  |       |j)                  dd|        |j                  |       ddd       | j+                         }	 |j-                  | j                  | j.                         |j1                  |       	 |j3                          |	S # 1 sw Y   dxY w# t4        $ r |j7                          Y |	S w xY w# 	 |j3                          w # t4        $ r |j7                          Y w w xY wxY w)z.Send an email with a file attachment via SMTP.ru  r  r|  r  r  r  rv  r}  rx  r  Tr  ry  r  Nr  r   r   rw  r  r   r  r  r  r   r  )r   r  r$  r   r  r	   r  r  r  r  r  r   r   r]  r  r   r  r  r
   r  r  rF  rR  r  r  rW  rm   rE   )rS   r  r  r  r  r   r?  r|  r  r  r   fnamer  r   r   s                  r'   r  z(EmailAdapter._send_email_with_attachment[  s    ommFD	""&&w3'')^4!!%(WI&G I'',/!0C /C 40FDJJL,,Sb12!D4K4K4M3NaP"LJJxgw78 O#QVV!T] 	aM>:DQVVX&""4(OO15J5'3RSJJt	 !!#	JJt}}dnn5c"		 #	 	  

		 

sO   A'G7H
 G* G'*HH
H<HH<H96H<8H99H<c                 p   K   | j                   j                  |i       }|d||j                  dd      dS w)z'Return basic info about the email chat.r  r|  r}   )r]  r   r  r|  )r$  r   )rS   r  r?  s      r'   get_chat_infozEmailAdapter.get_chat_info  s?     ""&&w3wwy"-	
 	
s   46r:   N)NNr/   )NNN)Ng        )'rV   rW   rX   __doc__r   r  r2  r8  r9  rF  r1   rC   rf  r[  rh  r   r   r   r   rm  staticmethodr  r  rn  r   r   r  r  r  r  r  r   floatr  r  r  r  r  __classcell__)r&  s   @r'   r  r    s#   E>I~ >I@U((,w|| (,T 5: BT Bd BH
-	53`T$sCx.%9 `D 
 
 
 
$ 
 
q)S#X q)4 q)n #'-1;; ; 3-	;
 4S>*; 
;$C  *.	)) ) "#	)
 
)V7 7c3h8P 7\` 7 "&"&-1@@ @ #	@
 3-@ 4S>*@ 
@* .2 2W2W U38_%2W 4S>*	2W
 2W 
2Wh44 4 I	4
 
4t "&#'"&;; ; #	;
 C=; 3-; 
;< $(22 2 	2
 C=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       xs i }
|
j                  d      xs t        j                  dd      }t        j                  d	d      }|
j                  d
      xs t        j                  dd      }	 t        t        j                  dd            }t        |||g      sddiS 	  ||dd      }||d<   ||d<   d|d<    |	d      |d<    |j                  ||      }|j                  |j!                                |j#                  ||       |j%                  |       |j'                          dd|dS # t        t        f$ r d}Y w xY w# t(        $ r7}	 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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  ru   r   r}   r   r
  r   r  587r  rN  zNEmail not configured (EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_SMTP_HOST required)r  r   ru  r  r  rv  Tr  ry  r7  email)r  platformr  )_errorzEmail send failed: )r8  rA  email.mime.textr   email.utilsr	   ra   r   r   r   r)  r   r/  r   r9  r<  rB  rR  r  rW  rm   tools.send_message_toolr
  )pconfigr  r_  r  r  r  r8  _sslr   r	   r  ru   passwordr
  	smtp_portr   serverrs   _es                      r'   _standalone_sendr    s     (&GWb)/REii	"Dbii&DGyy)2.H		+&J"))4Er*JI		"3U;<	 9-.ijj8w1FD	'I 40Fi3 ; ; =>Wh'C WII% 	" 	&  8	8<+A3/00 	821#677	8	8s   BF.E 0F.BE+ F.E(%F.'E((F.+	F+5FF+F.F#F&F+F."F##F&&F++F.c                     t        | di       xs i }|j                  d      ryddlm} t	        |j                  d      xs dj                               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  ru   Tr   Nr   r}   )ra   r   hermes_cli.gatewaygatewayr1   get_env_valuer   )r  r  gateway_mods      r'   _is_connectedr    sM     FGR(.BEyy,**?;ArHHJKKr)   c                     t        |       S )zCFactory wrapper that constructs EmailAdapter from a PlatformConfig.)r  )r  s    r'   _build_adapterr    s    r)   c                 h    | j                  ddt        t        t        g dddddt        dd	d
d	       y)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   📧)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_platformr  r   r  r  )r?  s    r'   registerr/    sB    &)"KW/-1-!!  r)   r/   )rq   zimaplib.IMAP4r:   N)Fr  )Tr  rY  r  r  rP  loggingr   r   r8  r=   rA  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   ro   r   r   MAX_MESSAGE_LENGTHr;  r   r)  r  rN   r9  rP   r:  r[   r   rt   r  r1   r   r   r   r_  Messager   r   r   r   r   compiler   r   r   r   r  r  r  r  r  r/  rY   r)   r'   <module>r=     s  "     	 	   
  & . $ $ "   3 3  4 #			8	$  2A 1)	     	>
>
> > 	>
 ]]>>
 

G$$ 
 9D.	# 	 	 	
($ 
(	c 	c 	#I--55 ## #Lc c   " " "6 6 6 6& "**("--
 

YMM 	M=				"	"M=M= 	M=
 49M=d #9				"	"99 
$sCx.9xq
& q
J .8bLT L 
r)   