From 0ca890d22416806578516703fd9854cf1fe52c25 Mon Sep 17 00:00:00 2001 From: Abhishek Ranjan Date: Wed, 16 Jul 2025 11:01:14 +0530 Subject: [PATCH 1/6] feat: mail via smtp & few unit tests --- src/google/appengine/api/mail.py | 69 +++++++++++++--- tests/google/appengine/api/mail_unittest.py | 87 +++++++++++++++++++++ 2 files changed, 147 insertions(+), 9 deletions(-) diff --git a/src/google/appengine/api/mail.py b/src/google/appengine/api/mail.py index e7154c1..3337d9a 100755 --- a/src/google/appengine/api/mail.py +++ b/src/google/appengine/api/mail.py @@ -45,6 +45,8 @@ from email.mime.text import MIMEText import functools import logging +import os +import smtplib import typing from google.appengine.api import api_base_pb2 @@ -1197,20 +1199,69 @@ def ToMIMEMessage(self): return self.to_mime_message() def send(self, make_sync_call=apiproxy_stub_map.MakeSyncCall): - """Sends an email message via the Mail API. + """Sends an email message via the Mail API or SMTP. + + If the 'USE_SMTP_MAIL_SERVICE' environment variable is set to 'true', this + method will send the email via SMTP using credentials from other + environment variables. Otherwise, it falls back to the App Engine Mail API. Args: make_sync_call: Method that will make a synchronous call to the API proxy. """ - message = self.ToProto() - response = api_base_pb2.VoidProto() + if os.environ.get('USE_SMTP_MAIL_SERVICE') == 'true': + logging.info('Sending email via SMTP.') + mime_message = self.to_mime_message() + + # The Bcc header should not be in the final message. + if 'Bcc' in mime_message: + del mime_message['Bcc'] + + recipients = [] + if hasattr(self, 'to'): + recipients.extend(_email_sequence(self.to)) + if hasattr(self, 'cc'): + recipients.extend(_email_sequence(self.cc)) + if hasattr(self, 'bcc'): + recipients.extend(_email_sequence(self.bcc)) + + try: + host = os.environ['SMTP_HOST'] + port = int(os.environ.get('SMTP_PORT', 587)) + user = os.environ.get('SMTP_USER') + password = os.environ.get('SMTP_PASSWORD') + use_tls = os.environ.get('SMTP_USE_TLS', 'true').lower() == 'true' + + with smtplib.SMTP(host, port) as server: + if use_tls: + server.starttls() + if user and password: + server.login(user, password) + server.send_message(mime_message, from_addr=self.sender, to_addrs=recipients) + logging.info('Email sent successfully via SMTP.') + + except smtplib.SMTPAuthenticationError as e: + logging.error('SMTP authentication failed: %s', e.smtp_error) + raise InvalidSenderError(f'SMTP authentication failed: {e.smtp_error}') + except (smtplib.SMTPException, OSError) as e: + logging.error('Failed to send email via SMTP: %s', e) + raise InternalTransientError(f'Failed to send email via SMTP: {e}') + except KeyError as e: + logging.error('Missing required SMTP environment variable: %s', e) + raise InternalTransientError(f'Missing required SMTP environment variable: {e}') - try: - make_sync_call('mail', self._API_CALL, message, response) - except apiproxy_errors.ApplicationError as e: - if e.application_error in ERROR_MAP: - raise ERROR_MAP[e.application_error](e.error_detail) - raise e + else: + logging.info('Sending email via App Engine Mail API.') + message = self.ToProto() + response = api_base_pb2.VoidProto() + + try: + make_sync_call('mail', self._API_CALL, message, response) + logging.info('Email sent successfully via App Engine Mail API.') + except apiproxy_errors.ApplicationError as e: + logging.error('App Engine Mail API error: %s', e) + if e.application_error in ERROR_MAP: + raise ERROR_MAP[e.application_error](e.error_detail) + raise e def Send(self, *args, **kwds): diff --git a/tests/google/appengine/api/mail_unittest.py b/tests/google/appengine/api/mail_unittest.py index 7c90793..f5198d5 100755 --- a/tests/google/appengine/api/mail_unittest.py +++ b/tests/google/appengine/api/mail_unittest.py @@ -44,6 +44,10 @@ import six from absl.testing import absltest +try: + from unittest import mock +except ImportError: + import mock @@ -1627,6 +1631,89 @@ def FakeMakeSyncCall(service, method, request, response): mail.send_mail(make_sync_call=FakeMakeSyncCall, *positional, **parameters) + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp(self, mock_smtp): + """Tests that mail.send_mail uses SMTP when configured.""" + environ = { + 'USE_SMTP_MAIL_SERVICE': 'true', + 'SMTP_HOST': 'smtp.example.com', + 'SMTP_PORT': '587', + 'SMTP_USER': 'user', + 'SMTP_PASSWORD': 'password', + 'SMTP_USE_TLS': 'true', + } + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.', + cc='cc@example.com', + bcc='bcc@example.com') + + # Check that smtplib.SMTP was called with the correct host and port + mock_smtp.assert_called_once_with('smtp.example.com', 587) + + # Check that the SMTP instance was used correctly + instance = mock_smtp.return_value.__enter__.return_value + instance.starttls.assert_called_once() + instance.login.assert_called_once_with('user', 'password') + self.assertEqual(1, instance.send_message.call_count) + + # Check the arguments of send_message + sent_message = instance.send_message.call_args[0][0] + from_addr = instance.send_message.call_args[1]['from_addr'] + to_addrs = instance.send_message.call_args[1]['to_addrs'] + + self.assertEqual('sender@example.com', from_addr) + self.assertCountEqual(['recipient@example.com', 'cc@example.com', 'bcc@example.com'], to_addrs) + + # Check the message headers + self.assertEqual('A Subject', str(sent_message['Subject'])) + self.assertEqual('sender@example.com', str(sent_message['From'])) + self.assertEqual('recipient@example.com', str(sent_message['To'])) + self.assertEqual('cc@example.com', str(sent_message['Cc'])) + self.assertNotIn('Bcc', sent_message) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_MultipleRecipients(self, mock_smtp): + """Tests that mail.send_mail handles multiple recipients via SMTP.""" + environ = { + 'USE_SMTP_MAIL_SERVICE': 'true', + 'SMTP_HOST': 'smtp.example.com', + 'SMTP_PORT': '587', + 'SMTP_USER': 'user', + 'SMTP_PASSWORD': 'password', + 'SMTP_USE_TLS': 'true', + } + + to_list = ['to1@example.com', 'to2@example.com'] + cc_list = ['cc1@example.com', 'cc2@example.com'] + bcc_list = ['bcc1@example.com', 'bcc2@example.com'] + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to=to_list, + subject='A Subject', + body='A body.', + cc=cc_list, + bcc=bcc_list) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + to_addrs = instance.send_message.call_args[1]['to_addrs'] + + # Check that all recipients are in the `to_addrs` list for the SMTP server + self.assertCountEqual(to_list + cc_list + bcc_list, to_addrs) + + # Check the message headers + self.assertEqual(', '.join(to_list), str(sent_message['To'])) + self.assertEqual(', '.join(cc_list), str(sent_message['Cc'])) + self.assertNotIn('Bcc', sent_message) # Bcc should not be in the headers + + def testSendMailSuccess(self): """Test the case where sendmail results are ok.""" self.DoTestWithStatus(mail_service_pb2.MailServiceError.OK) From 5d2a1467d9c631f239e22b06564f7908c2fbd48a Mon Sep 17 00:00:00 2001 From: Abhishek Ranjan Date: Wed, 16 Jul 2025 13:49:14 +0530 Subject: [PATCH 2/6] tests: add more unit tests for send mail smtp path --- .coverage | Bin 0 -> 135168 bytes tests/google/appengine/api/mail_unittest.py | 216 ++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 .coverage diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..247d9db6c84948acffb5088d4f1485b155c4b2d3 GIT binary patch literal 135168 zcmeHw34k1BweH{D)oX3DFPVgNHbN36ok<8Jgpdgdgh1E{yL76%Yi2s>jqaW#!{RbAaP;ql%DP6<_zQ{B~F zJzstQ`Om+dbN(|<+m@~B@sVOVuT|r#SR3>C*l;|~7>mNcdGOCV!tg?{-oV-CczvLk zQMP>NL$Z1nlR{TB^+Nd+bx`_%e6aYil#jk${9ss%rr`tc@EGtI@EGtIcw;kQoF;{P z`}=*h%c@!`r&r5bS})h7yy^5~H=cQH{LGEVY&$kyzZO3v7KdYSFupNfF7An!^m2S8 zo73ajLMEHms@cM5ygH^^-yW~%8Eoj_8Z>qI#pV2de3i^*;I67Z3O`XQXY*QlBECzX zIFx_E{A*QxZ?%2_eniiX7VsPKL(JQk#LN1KUe*g~y;4VUNH()1w&aR^v2br^r|&AA zKa*BY!~bH_)4+wsqih|4A4(U?8AP8dYlZZfUO6xBc_?5r~hiwv!d6)j#gzX(W4wGF`nhIlGhOf~Bo*O-b96$EU7^|g`lLK>?|1)m{Z z+^v_jQGL1jY27SPD-x*AP&Cwg?A!yXj;#X8A^T;_=nsW^2L^n#C|4u1w9HeuNrkME zoWYD*ALVteGG4X}$x9hY>yCz%wCafY2|UhVIMlmw;6RUKelIy>zK9rMA>7;D?KA2% z0__t13pPCjUTIW;mWJTv!!K#ovG{cCco~2%4u*Qybsq>gLP!pw3|(=)pF_T?pR=C_ zqmTarO_1?Q1M-Rb#Nci%2iBydWy?@y!3Nh`fH;0JqrsQ8RB^n@^%viA^~AQPfw%qx zjrSJ!@U12M>rlNV?6 zP$?@=MO%K;`cd;!v{=^fFE0Tn_^Wv>Q?Kq=D2`v;Gp0}bxq54~S*z;RY+jG!S2!51 zSbMZeJYCk|LugBvEDpSs8enN9hfv}uIf7j|W3DgUyKI@y7&j|p-OeN{(5UA%3%2>z z-=rihA2#&m6fn5Z+i~g%a_67}-`4*E8n=20rs^zM-!l6;u_ZE~^rhIL2)cxw?>LbxdqKU|j5f(0leihmtyf?T)xIqX6cKILjpWwU8 zx0Ky%{ranPHev$;tMw1t*AClJy(3SX#Xb`7S-v;w?NBMNUIGWQFTB)QLMj`eMUS$(` zEt^Z~<#MrHfyfd=odsYPkb*HEG+9cmL_}ggAR3@T1QaDxT1B_e1o{AtNQDLq3pwfQ z1r)0(P+;^@r3z7liAu4|<9&dcBm@K(GkO`Jb@u?Y1T8eZP|a2+lBtQRUa<;MO;3*L5V={W zs#6AF%iI9WsoC=-K(&G) z;pmBrApk|v@3lZVHChZ(p+S>i(^D>`QG?n9K(mNSgKRXF>Uau_R0$!)0sx6@6gM)S z%Ozo82T5!YOxO=#I;r?^>P^50NM4Qc5{$+VGPZ-9ZL9-1%a{+c*TDRJT{#dPr-Eo- z4WezpAG?~xA|JtNfFAV%@$2Hj(XEk>C|`^ISvgetv;5}hqtO+ivqDb??+XqIHwJQo z;D1>9q7;w3KjI4);jUNpFhrr&@1rWL=JEdkRIS+F6Dq)CR~9L0xx_|N9L= zsEK4~%<+Gp(J$0ilc2C$&jXDAdyO8UmLP^U!}!0)=oD&8+=0s;XLq}SEW1P-V<-l2|p$fHmB%ER4HrTR;=9pkiHRcMn4PBOp|agP?+v@C9J1dbyzG5-@!RtyC$UC_t+PE8fX$z62|jIKOw0m;Vn+ z$vZp-JO(@lJO(@lJO(@lJO(@lJO(@lJO(@lUcU?kd;un7{J&3qk-`7o;W6Mb;4$Da z;4$Da;4$Da;4$Da;4$Da;4$Da@H%855Rjv|{yz{qlc~=`-2bEME_I8#R$VB+DF0f1 zQ2w@jhx}3bdim}0S@N*FMmkTLFG=E);(KFP$M(c@xXn8}20R8l20R8l20R8l20R8l z2ADb^_@eOvNG!FAAAz=6aAE!|#-8c?#Vse_cE;3(u70?(w4YzucjV0{-}d8APEGl0 zzP_L@dZoV)l2Nf|rv6jCd*9@gc?<)wlk5ieyIX|g{cD!^UVKtm?_|H4^D~fe*c;mUiiCj z|38jzXUo|I55)EdhyVEC*DwB8Hd**AoBZa60An{O9nWq4(A4*zjqTn8e`v=oKK394 z+OFu?kz_k21a`rbvwVN|w*{_Cw6Sh^^2H}UGJG!LVRxU+fS*fu@7dN16hIauNXyOG zA38ob9RJ-^deg%ABv1l}u(6G`!N%f~lauidct82(tsR088D_)q#&5uvBExaGlfQ4< zxU2_wKBtHC3`Fi(Q+?<2C(rr9kAGeL%?&pnIda<>-}&7m7Q=zAZY-4U`j;OWS_j4Q zkEwl!cfr*{7hJvO9{$za;BH=IGo1cz{n*r$aegN}$rXopLSYI_xvuZJsi|9@yW#mI zaA6pq0iURi;e)LaCz+oe`Z?x}B{HgkXZOlIm9|F8#_(3=XxYG~6bjQN_Z}{M~ zuWdNg0jww;zOHuUti8?|r{#>PsSPXJ;OYqcNZTC;?HGBAO+p;@Qr2;_k9~sq{Gh-Q zf82+WIQ$t{bQ1oGsKPycDr1iyz9A4(;7yOh*k`BuMHxSUf!faxW}$Wo-YkS$BkXJz zSj>ha+oB>|>Js6S&>w@Bhrya|8+%~Is#SfVx4e1ihW^m|GNDj=6mN_IBX>vHGcRmr zzRB962v)ZffF^+Y+#jdJ_%OS2a&q7Bdlo*|ad(*Ay#HRn(*{%kd6W1z{Y*I(Ve>l} zuS)f|Y;t&V7!Kw`_D^L{7yQ5+a)uWKcEvI5Zua&S%zw}1I|ApxB`BWZ;VyW@JMm-Y zTXGL;8ydcc^|0YSxaICR8=kx;{y^U_d&}%$wt1LE|1>oh)a!~){&k^kn6Ve9rc|^L zB31>)He*Q**Gn!AkO9kcQ&V%{q49{LnA&$Tp5U0|5&^RPQ&U?QXgD6g1RRit13%!$ zWM>$tI|Rb#BO@cBr}ltN6IO+!5L}!E3`E}IW4`%8cy}B`(ML*?EBC|IIGcp~ffS4a z08oBmcuc^WeJcEN8{03#r|^`0!yg0L1~0FKuk8r6_p!)bn*{*ppPa-yS)8lMrMJV& zN%(&HokKg`IaGhc1V6vHf-#BhfNOBTUtJ+|2LR2XSOhDgz(zd6M&P4>=!eUFa1Z>y zDd-cTQN@Sr{{!l=408YdRsB6={(DmWiTbemJ@wn_UFsLr&#JenA5w2r->1Gqy;8kQ z-K`eYF;!E~Q_oPh!EN5*G2k)aG2k)aG2k)aG2k)aG2k)aG2k)aG4RG=0H*?c0|Wdt zYZgEC_w!R@>5R_KXrHWQ&$&1b$0SoM+ZN(xARk58$YQkKPd`7$ud7leCoj` zLKaN;v#}UIMWg%_iSSc6%uk^ZKLvyQBnbQz2=J5NFTextmR|8KBD_a4_{z+=E;z+=E;z+=E;z+=E; zz+=E;z+>PI#{kCvKOvrt{{Med{{%Szex?3G{Tajn{8)WZ{jPeC`c3s~5C?FFdb@h7 z`eF40>h%x{aJBkY^$PWJbwaH|Jity>S9hrAt7oaFK}^6F^%!-%x<*~4E?1YR3)Q*m zEVWy0Q$;nT`ji)y|5X00{9bul`K9u6<=>P?lpiYJQ|?vnR=%!$N%@@eY2`NMBg)On z4azmjyOg&nS1S9IiDyW}s+pO-%)e**Fvd{Dko{ulY( zkloY41yk0&+9+H>Ii{!a-zuYOya#;3BFG$Zyf0TYL z{Zjgw^qBOp^nK}G>6_A5r7uXIk#3VdEZrnsE4^EKn{kYbp3dcIT%OA1DO{e+7%n$*Im{*GE&#MgbGe?&qqtnh<&j*j<#G*|M{s#Km#euvjLTJA zuHC#V!;(QDjk!p%_Imf*~<;u!+m8&Y(RIaF8Pq~_M zE#*qeb(E_p*HEsYTtB&ba_!{G$#s*fCf7`^m|QQpT5_%AO38JSt0dP*u8>?GxjJ%f zIFcz!9~Cj)t>CdS~&y2jy48_+FD|Dvv=DBvXa&t05|BWtv~^sUe>C5vNTa0D`Kf0 zDtw%Up{^S3LTwQ-hh}}i!UbfjgO9N=6f4~pxzp-H1)ynpG$e<+$bP`)!etFXtpelR zvt(K!x~ORLS{9XVy^BnVzBjFxs<5*=GI*5HCe#*8V^Gr)E6y&KcO_Eeg-lM*48nHJ zyR&J%G6*$&r=G4>rhk&nYoqW5*m!p^J*E}lbt0EdCE$IcQcfpEi^b8Lj@xJJh0$z5 zPfSy`%!mev*11U0s>NM8r|C$0bLiK6+v;gFIW~u0YxKY_^6n_TG$<786V10^mvyeOTCUPe@JAsJ z>56tr;Mu|mb|TnYjfFyOD*78G#KVWF#L zk~neW<(ytf7ccz@4 zN*es+kCE<_=8EqU{jpN?k!T`vV}ykZpE0x%0m!8DI*4Y3~(4NxJ1jw#n~v*Tq1`T&hcg$4@? z6dHEU0Tin#P~a?61!j-+LxN$!g##rEqo3kUId>T-p+Jbdn9V9q^Eu%5gB0e7YAF z<}Gy+V6&*2Ou-P2-w_cnC{?8*raTT4;(0v*%h6Tv6G`B}234m=0YpO-#IzzB)%4_; zu4PQBI%NR1%niVtnmu0vR9h)fp&k`9SQ9U4pw@Y<088WL=~u8!`$YhGgnK|b2~=Z% zjm$JOEN7!%7mWg%d^4g2m>IJzJX;PK<{C|0@K5d z5`)a=MEzb1lvAU{AQc)k2{t|TsbO$i12l`MG{{DSOPS_ez7Y!mB(hQ5$apT7tS=a$ zZ4CPXOeYmTPQ8K6Mv2Bt4iq^4pT$mNYDsxiSuNir`=zw_fVez%ZHz@nB0q>E!tV(^ z8@eEPS8%TImcY}26aAm_cls`8{{c76bQo=`sT6RUN1*9HZ1f%mkXBKY`7}t)Q3yuI zDnLeJUZ)|morjHql>llTEnR$$3QQXBrzRWbrWxmRTX0( z;i5t{Jv(g&N~32vpd%TDH0ps}sQd-Y-ePkxYtThy+7eM>%sCXmZE%UgS$>_3h!Ue` znTtx*;Z1DxEd^jClY}#<8@K>9+$W9MO90w>*OjU1N^A@q0`T0@HZ*)<>mIvC&texT zTvMZUFaRU&fi&AUbKT77SOmz3dmuJ4t0nDO=%R9TEYBJP3jml~)Zc2StW`#%Yd)YG zbcyY-7u${5^8l7xOd2Q6xu$LO#9gFpRx65qkKVZe%q>R4>D6J(IS7!srQ=a37LpK5 zm(1qT>4Zc{V^JS*mGtJsd4lef_`6)SWQ?UoYP(w@drPr^GX2 zUyIF)UK{mAQsD=~8$(|XEegIj7!XQ4pO<|{S z<)e^EB`zA*xxbAeo&r!vme4IIR)7Lmj+3dB!$O08ok?tlc|Gbn3Gk3KC>EZ~PKcN- zCiB@0gxl`X%IJk;bZi4yBz+cymC6-U^>u#x6@qF%5kM`bQfeDY(_Bg034n=YG8l=1 zfep;r>lyn6($u!q6_6Yz&=8LYNMtr(^rBo-nYC}B)FX)SNrM08D1@OydtnBN8R~I> ziDc0Z^$2ZJ3e?DnD10m+BALcVL{22J&47etb)#A=<|>I|#j$?azKKeC>R|TfbIFoc zMh6VL5t)4qU?S=M*D>MRPd&8)5LFn-jwbC)58Q5Vq@@hHBIA0pK^$U=4g)5VCFwe4 zPW|rL09c5pX3cysO<9PHo}*m@tSJkT(Y79dk+iyvF22c_nP$c4Im$I$*_E&E5Va0~ zk+dw+xT>dFF*=R}L?oTdX^8AJD@NB^07cRfp9acl8@ks3EE12_X|SAWR*a4#TvLV( z-=5CH0ThWt+cdHo>8oPGcUd2}@(T+BHwGeV8D;?z^7|pn{VxBA_>j27x72rk?7h+7 zMz=-oj;s!UGTa-g1)m9?2e;kSG}6b66sum*fK5Y&kjq!}7hi4$CiJVxWWmv{njW*l zpKBOkvX%@^V|~E$BwF;P&2%?yE*zsBLrscQ%VH?!oLU zh6>3+$Phvo74@v);>K>oFjg<_za^W;OT_(yF?#PjvasLjo|6&GR=9*rqZ)vCjkGe0 zTt}DIjvG}gM5dwXF$}0(>;j&6WK5Rl^I}&bP4ip;O3UPVo<#N*BPP_k=@DULbAu6u z5PLE#>2k4PwaV{6T6)Q=*z(b2Z`5(6_#TKH>Rt5 zd4?js2mvZ#D7yd}NnDi$t+9e#pZDAefJo*&EkN}elpHG-cX33WS*qzN3z3-%4}x{F zym7;Vlg9uRiMx!2s$pUfPtPdeAz2Nv@RW5}F#;zIy|6nAJ>fizTR2f8)I@P7W-SLh zY#<&{2RtN;>K0K}EE-3Xp+=L|aye8bL=#Q}8nVS}aDRe?ET zG%3zOZvs#osn~IVYP@XDuoAfd0FpX9+BH6MJ~b5EF+@5Kpv<8vHgp5EM>(k4v2y_q znJq*(X7?4rM9!fuCfg8-X9E`*@&b94cIEVXizS{#jRsxh&?OCvN!ku*NL5(h z4IRK-v4~l74e?BBavVclLpp<+oaP4obbv!zzw?z*2tVN^6*>*TkTvL7sX&cEWnwUX z?-sVO@bzJTXgv5dtn5D|Y*F*d<)4;j>?{*v(8vf_6Vf$3hd!FqXV2G$cn20%0bkLw55h!mPY^SbaX-J zCL@T0F(-=7p{?i~in+%TXwE70XdL6CvkcO9q0!Af4V_8nkm`$bpthYZEj}6@2uj?I2XF^XMy4w9NEiMU3ow`QvR6SC0!x@Mm#6>Kx{Djp{N8Od56b<$AHIx$AHIx$AHJcE5ZO> z|Mh^03N}$=njsUwM!FGMohN5@iOqDIlf9fw4#&{Nf=tVrFif|EV6FMI_;>E4+k(tO zG_&X8D286rMDAI@ntgM3(XSsixl)c`;KVYLxhspH89Z~d^crq?dz)L?sPr0cA!^>r zM!}$*)uaVz0c&-P&(-t)4Kp>V+@~y+-zoh;I#;|`TpGJF`s?WS$hW}z|Aw$HlnOo^ zJVv-vSQ_|1K=vEH-}yB5LpIEqA;J_dk$Vuj0ZwR;aVRyO&B3-oFiU4B2JQ_qkHV=8 z;WWZ-t;kIyY~cPNB;LM=2vVJn=#a+%$!Rxma}bgobZT$T_Hda*QOg^ZBF01b)H5~9H}%4Jii$KpQ9QUWl} z?avMAazI1k1BkS61DJ#TgY|-G0WSkMWCjqtM#muo2>P&C$-HM@3V=xRzuDF}ka`%uOVbpaAz#~bmWW#YZ8`(le--|llb+MaZ+4*wS34lcsx7J`wOINdy^SP8- z$s4u3fQY0`Yaq%L)8iP3WnKyb{-9#Xwvcp^3EA;wiQwfy-3$>#Rm`EapY$Oi5YVM9Va1nr7 zN2_TzJls`yHv~+=g(4rk43u1jZdG#-Q-2ZAk?bmJGv+8zuK#vx*;4>)Bw@KTVS{qD zjK)EE0Jf1<;ij23htAd4|3AglaftuFKz=~pD1BNI#fjK&V{eMyA6*mq)GIRG@BN6! zfX9HxfX9HxfX9Hx!2bsWbl#=bDwr2zSWYKKi^b8L4%?2G>U%0AEOuZr5`tTu`?Py8 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 abkBw}GeoGeAJR5LRx}rK))IBe9{4Y-=AAVF literal 0 HcmV?d00001 diff --git a/tests/google/appengine/api/mail_unittest.py b/tests/google/appengine/api/mail_unittest.py index f5198d5..525cf56 100755 --- a/tests/google/appengine/api/mail_unittest.py +++ b/tests/google/appengine/api/mail_unittest.py @@ -1713,6 +1713,222 @@ def testSendEmailViaSmtp_MultipleRecipients(self, mock_smtp): self.assertEqual(', '.join(cc_list), str(sent_message['Cc'])) self.assertNotIn('Bcc', sent_message) # Bcc should not be in the headers + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_HtmlBody(self, mock_smtp): + """Tests that mail.send_mail handles HTML bodies correctly.""" + environ = { + 'USE_SMTP_MAIL_SERVICE': 'true', + 'SMTP_HOST': 'smtp.example.com', + 'SMTP_PORT': '587', + } + + text_body = 'This is the plain text body.' + html_body = '

This is the HTML body

' + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body=text_body, + html=html_body) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + # Check that the message is multipart/mixed, which contains a multipart/alternative part. + self.assertTrue(sent_message.is_multipart()) + self.assertEqual('multipart/mixed', sent_message.get_content_type()) + + # The first payload should be the multipart/alternative message + body_payload = sent_message.get_payload(0) + self.assertTrue(body_payload.is_multipart()) + self.assertEqual('multipart/alternative', body_payload.get_content_type()) + + # Check that there are two payloads (plain and html) inside the alternative part + alternative_payloads = body_payload.get_payload() + self.assertLen(alternative_payloads, 2) + + text_part = alternative_payloads[0] + html_part = alternative_payloads[1] + + self.assertEqual('text/plain', text_part.get_content_type()) + self.assertEqual(text_body, text_part.get_payload()) + + self.assertEqual('text/html', html_part.get_content_type()) + self.assertEqual(html_body, html_part.get_payload()) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_WithAttachment(self, mock_smtp): + """Tests that mail.send_mail handles a single attachment correctly.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + attachment_data = b'This is attachment data.' + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.', + attachments=[('attachment.txt', attachment_data)]) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + self.assertEqual('multipart/mixed', sent_message.get_content_type()) + self.assertEqual('sender@example.com', str(sent_message['From'])) + self.assertEqual('recipient@example.com', str(sent_message['To'])) + self.assertEqual('A Subject', str(sent_message['Subject'])) + + body_part, attachment_part = sent_message.get_payload() + + self.assertEqual('text/plain', body_part.get_content_type()) + self.assertEqual('attachment.txt', attachment_part.get_filename()) + self.assertEqual(b'This is attachment data.', attachment_part.get_payload(decode=True)) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_WithMultipleAttachments(self, mock_smtp): + """Tests that mail.send_mail handles multiple attachments correctly.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + attachments = [ + ('one.txt', b'data1'), + ('two.txt', b'data2'), + ] + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.', + attachments=attachments) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + self.assertEqual('multipart/mixed', sent_message.get_content_type()) + self.assertEqual('sender@example.com', str(sent_message['From'])) + self.assertEqual('recipient@example.com', str(sent_message['To'])) + self.assertEqual('A Subject', str(sent_message['Subject'])) + + payloads = sent_message.get_payload() + self.assertLen(payloads, 3) # 1 body + 2 attachments + + self.assertEqual('one.txt', payloads[1].get_filename()) + self.assertEqual(b'data1', payloads[1].get_payload(decode=True)) + self.assertEqual('two.txt', payloads[2].get_filename()) + self.assertEqual(b'data2', payloads[2].get_payload(decode=True)) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_WithHtmlAndAttachment(self, mock_smtp): + """Tests handling of both HTML body and attachments.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.', + html='

A body

', + attachments=[('attachment.txt', b'attachment data')]) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + self.assertEqual('multipart/mixed', sent_message.get_content_type()) + self.assertEqual('sender@example.com', str(sent_message['From'])) + self.assertEqual('recipient@example.com', str(sent_message['To'])) + self.assertEqual('A Subject', str(sent_message['Subject'])) + + body_part, attachment_part = sent_message.get_payload() + + # The body part should be multipart/alternative + self.assertTrue(body_part.is_multipart()) + self.assertEqual('multipart/alternative', body_part.get_content_type()) + + # The attachment should be correct + self.assertEqual('attachment.txt', attachment_part.get_filename()) + self.assertEqual(b'attachment data', attachment_part.get_payload(decode=True)) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_WithReplyTo(self, mock_smtp): + """Tests that the Reply-To header is handled correctly.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.', + reply_to='reply-to@example.com') + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + self.assertEqual('reply-to@example.com', str(sent_message['Reply-To'])) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_WithCustomHeaders(self, mock_smtp): + """Tests that custom headers are handled correctly.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + headers = { + 'List-Id': 'some-list ', + 'References': '' + } + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.', + headers=headers) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + self.assertEqual('some-list ', str(sent_message['List-Id'])) + self.assertEqual('', str(sent_message['References'])) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_NoTls(self, mock_smtp): + """Tests that TLS is not used when disabled.""" + environ = { + 'USE_SMTP_MAIL_SERVICE': 'true', + 'SMTP_HOST': 'smtp.example.com', + 'SMTP_USE_TLS': 'false', + } + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.') + + instance = mock_smtp.return_value.__enter__.return_value + instance.starttls.assert_not_called() + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_NoAuth(self, mock_smtp): + """Tests that login is not attempted when credentials are not provided.""" + environ = { + 'USE_SMTP_MAIL_SERVICE': 'true', + 'SMTP_HOST': 'smtp.example.com', + } + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.') + + instance = mock_smtp.return_value.__enter__.return_value + instance.login.assert_not_called() + def testSendMailSuccess(self): """Test the case where sendmail results are ok.""" From a9c1f1315a98e957ca6cd50ffa4649fcb9a680b2 Mon Sep 17 00:00:00 2001 From: Abhishek Ranjan Date: Wed, 16 Jul 2025 16:33:58 +0530 Subject: [PATCH 3/6] test: Add comprehensive tests for SMTP mail sending --- tests/google/appengine/api/mail_unittest.py | 173 +++++++++++++++++++- 1 file changed, 172 insertions(+), 1 deletion(-) diff --git a/tests/google/appengine/api/mail_unittest.py b/tests/google/appengine/api/mail_unittest.py index 525cf56..5b7429c 100755 --- a/tests/google/appengine/api/mail_unittest.py +++ b/tests/google/appengine/api/mail_unittest.py @@ -1756,7 +1756,80 @@ def testSendEmailViaSmtp_HtmlBody(self, mock_smtp): self.assertEqual(text_body, text_part.get_payload()) self.assertEqual('text/html', html_part.get_content_type()) - self.assertEqual(html_body, html_part.get_payload()) + self.assertEqual(html_body, html_part.get_payload(decode=True).decode('utf-8')) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_AttachmentsOnly(self, mock_smtp): + """Tests sending an email with only attachments.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + attachments = [ + ('one.txt', b'data1'), + ('two.txt', b'data2'), + ] + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='', + attachments=attachments) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + self.assertTrue(sent_message.is_multipart()) + self.assertEqual('multipart/mixed', sent_message.get_content_type()) + + payloads = sent_message.get_payload() + self.assertLen(payloads, 3) # body, and two attachments + + body_part, attachment_one, attachment_two = payloads + self.assertEqual('text/plain', body_part.get_content_type()) + self.assertEqual('', body_part.get_payload()) + + self.assertEqual('one.txt', attachment_one.get_filename()) + self.assertEqual(b'data1', attachment_one.get_payload(decode=True)) + + self.assertEqual('two.txt', attachment_two.get_filename()) + self.assertEqual(b'data2', attachment_two.get_payload(decode=True)) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_HtmlBodyOnly(self, mock_smtp): + """Tests sending an email with only an HTML body.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + html_body = '

Just HTML

' + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='', + html=html_body) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + self.assertTrue(sent_message.is_multipart()) + self.assertEqual('multipart/mixed', sent_message.get_content_type()) + + # The first part of the mixed message should be the multipart/alternative. + body_payload = sent_message.get_payload(0) + self.assertTrue(body_payload.is_multipart()) + self.assertEqual('multipart/alternative', body_payload.get_content_type()) + + # The alternative part should contain both the (empty) text part and the html part. + payloads = body_payload.get_payload() + self.assertLen(payloads, 2) + + text_part, html_part = payloads + + self.assertEqual('text/plain', text_part.get_content_type()) + self.assertEqual('', text_part.get_payload()) + + self.assertEqual('text/html', html_part.get_content_type()) + self.assertEqual(html_body, html_part.get_payload(decode=True).decode('utf-8')) @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithAttachment(self, mock_smtp): @@ -1892,6 +1965,29 @@ def testSendEmailViaSmtp_WithCustomHeaders(self, mock_smtp): self.assertEqual('some-list ', str(sent_message['List-Id'])) self.assertEqual('', str(sent_message['References'])) + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_WithAttachmentContentId(self, mock_smtp): + """Tests that attachments with Content-ID are handled correctly.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + attachment = mail.Attachment( + 'image.png', b'image data', content_id='') + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.', + attachments=[attachment]) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + _, attachment_part = sent_message.get_payload() + + self.assertEqual('', attachment_part['Content-ID']) + + @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_NoTls(self, mock_smtp): """Tests that TLS is not used when disabled.""" @@ -1929,6 +2025,81 @@ def testSendEmailViaSmtp_NoAuth(self, mock_smtp): instance = mock_smtp.return_value.__enter__.return_value instance.login.assert_not_called() + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_WithUnicode(self, mock_smtp): + """Tests that unicode characters are handled correctly.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + + sender = u'J\xe9r\xe9my ' + subject = u'Un sujet avec des caract\xe8res sp\xe9ciaux' + body = u'La decisi\xf3n ha sido tomada.' + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender=sender, + to='recipient@example.com', + subject=subject, + body=body) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + self.assertEqual(subject, str(sent_message['Subject'])) + self.assertEqual(sender, str(sent_message['From'])) + + # The SDK wraps even single-body messages in a multipart container. + self.assertTrue(sent_message.is_multipart()) + body_part = sent_message.get_payload(0) + + # Decode the payload of the body part to compare content. + decoded_payload = body_part.get_payload(decode=True).decode('utf-8') + self.assertEqual(body, decoded_payload) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_WithAmpHtml(self, mock_smtp): + """Tests that AMP HTML is handled correctly.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + + text_body = 'Plain text' + html_body = '

HTML

' + amp_html_body = 'AMP' + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body=text_body, + html=html_body, + amp_html=amp_html_body) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + # The SDK behavior is to wrap the bodies in multipart/mixed. + self.assertTrue(sent_message.is_multipart()) + self.assertEqual('multipart/mixed', sent_message.get_content_type()) + + # The first part of the mixed message should be the multipart/alternative. + body_payload = sent_message.get_payload(0) + self.assertTrue(body_payload.is_multipart()) + self.assertEqual('multipart/alternative', body_payload.get_content_type()) + + # The alternative part should contain the three body types. + payloads = body_payload.get_payload() + self.assertLen(payloads, 3) + + text_part, amp_part, html_part = payloads + + self.assertEqual('text/plain', text_part.get_content_type()) + self.assertEqual(text_body, text_part.get_payload(decode=True).decode('utf-8')) + + self.assertEqual('text/x-amp-html', amp_part.get_content_type()) + self.assertEqual(amp_html_body, amp_part.get_payload(decode=True).decode('utf-8')) + + self.assertEqual('text/html', html_part.get_content_type()) + self.assertEqual(html_body, html_part.get_payload(decode=True).decode('utf-8')) + def testSendMailSuccess(self): """Test the case where sendmail results are ok.""" From 36b0f2716fb137c7474ba95196bdefdff96ad8df Mon Sep 17 00:00:00 2001 From: Abhishek Ranjan Date: Wed, 16 Jul 2025 16:38:08 +0530 Subject: [PATCH 4/6] feat: Handle admin emails and add more tests --- src/google/appengine/api/mail.py | 17 +++--- tests/google/appengine/api/mail_unittest.py | 60 +++++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/google/appengine/api/mail.py b/src/google/appengine/api/mail.py index 3337d9a..93b3fa1 100755 --- a/src/google/appengine/api/mail.py +++ b/src/google/appengine/api/mail.py @@ -1217,12 +1217,17 @@ def send(self, make_sync_call=apiproxy_stub_map.MakeSyncCall): del mime_message['Bcc'] recipients = [] - if hasattr(self, 'to'): - recipients.extend(_email_sequence(self.to)) - if hasattr(self, 'cc'): - recipients.extend(_email_sequence(self.cc)) - if hasattr(self, 'bcc'): - recipients.extend(_email_sequence(self.bcc)) + if isinstance(self, AdminEmailMessage): + admin_emails = os.environ.get('ADMIN_EMAIL_RECIPIENTS') + if admin_emails: + recipients.extend(admin_emails.split(',')) + else: + if hasattr(self, 'to'): + recipients.extend(_email_sequence(self.to)) + if hasattr(self, 'cc'): + recipients.extend(_email_sequence(self.cc)) + if hasattr(self, 'bcc'): + recipients.extend(_email_sequence(self.bcc)) try: host = os.environ['SMTP_HOST'] diff --git a/tests/google/appengine/api/mail_unittest.py b/tests/google/appengine/api/mail_unittest.py index 5b7429c..3102700 100755 --- a/tests/google/appengine/api/mail_unittest.py +++ b/tests/google/appengine/api/mail_unittest.py @@ -2025,6 +2025,66 @@ def testSendEmailViaSmtp_NoAuth(self, mock_smtp): instance = mock_smtp.return_value.__enter__.return_value instance.login.assert_not_called() + @mock.patch('smtplib.SMTP') + def testSendAdminEmailViaSmtp(self, mock_smtp): + """Tests that admin emails are sent to the list in the env var.""" + admin_list = 'admin1@example.com,admin2@example.com' + environ = { + 'USE_SMTP_MAIL_SERVICE': 'true', + 'SMTP_HOST': 'smtp.example.com', + 'ADMIN_EMAIL_RECIPIENTS': admin_list, + } + + with mock.patch.dict('os.environ', environ): + mail.send_mail_to_admins( + sender='sender@example.com', + subject='Admin Subject', + body='Admin body.') + + instance = mock_smtp.return_value.__enter__.return_value + to_addrs = instance.send_message.call_args[1]['to_addrs'] + self.assertCountEqual(admin_list.split(','), to_addrs) + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_AmpHtmlBody(self, mock_smtp): + """Tests that mail.send_mail handles AMP HTML bodies correctly.""" + environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + amp_html_body = 'AMP for Email is awesome!' + + with mock.patch.dict('os.environ', environ): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.', + amp_html=amp_html_body) + + instance = mock_smtp.return_value.__enter__.return_value + sent_message = instance.send_message.call_args[0][0] + + self.assertTrue(sent_message.is_multipart()) + self.assertEqual('multipart/mixed', sent_message.get_content_type()) + + body_payload = sent_message.get_payload(0) + self.assertTrue(body_payload.is_multipart()) + self.assertEqual('multipart/alternative', body_payload.get_content_type()) + + payloads = body_payload.get_payload() + self.assertLen(payloads, 2) + + amp_part = next(p for p in payloads if p.get_content_type() == 'text/x-amp-html') + self.assertEqual(amp_html_body, amp_part.get_payload(decode=True).decode('utf-8')) + + def testInvalidAttachmentType(self): + """Tests that an error is raised for blacklisted attachment types.""" + with self.assertRaises(mail.InvalidAttachmentTypeError): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.', + attachments=[('virus.exe', b'some data')]) + @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithUnicode(self, mock_smtp): """Tests that unicode characters are handled correctly.""" From c5b91e0a731fb6f6d63dca617936007a7076481f Mon Sep 17 00:00:00 2001 From: Abhishek Ranjan Date: Wed, 16 Jul 2025 22:43:47 +0530 Subject: [PATCH 5/6] feat: Add final error handling tests for SMTP This commit adds the remaining error handling tests for the SMTP mail sending functionality. - Adds a test to ensure `MissingRecipientsError` is raised when no recipients are provided for an admin email. - Adds a test to ensure `InvalidSenderError` is raised for SMTP authentication errors. - Adds a test to ensure a generic `Error` is raised for SMTP connection errors and missing `SMTP_HOST` configuration. - Corrects the exception type raised in the `send()` method to use the base `Error` class for transient SMTP errors. --- src/google/appengine/api/mail.py | 7 +- tests/google/appengine/api/mail_unittest.py | 73 +++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/google/appengine/api/mail.py b/src/google/appengine/api/mail.py index 93b3fa1..3172477 100755 --- a/src/google/appengine/api/mail.py +++ b/src/google/appengine/api/mail.py @@ -1229,6 +1229,9 @@ def send(self, make_sync_call=apiproxy_stub_map.MakeSyncCall): if hasattr(self, 'bcc'): recipients.extend(_email_sequence(self.bcc)) + if not recipients: + raise MissingRecipientsError() + try: host = os.environ['SMTP_HOST'] port = int(os.environ.get('SMTP_PORT', 587)) @@ -1249,10 +1252,10 @@ def send(self, make_sync_call=apiproxy_stub_map.MakeSyncCall): raise InvalidSenderError(f'SMTP authentication failed: {e.smtp_error}') except (smtplib.SMTPException, OSError) as e: logging.error('Failed to send email via SMTP: %s', e) - raise InternalTransientError(f'Failed to send email via SMTP: {e}') + raise Error(f'Failed to send email via SMTP: {e}') except KeyError as e: logging.error('Missing required SMTP environment variable: %s', e) - raise InternalTransientError(f'Missing required SMTP environment variable: {e}') + raise Error(f'Missing required SMTP environment variable: {e}') else: logging.info('Sending email via App Engine Mail API.') diff --git a/tests/google/appengine/api/mail_unittest.py b/tests/google/appengine/api/mail_unittest.py index 3102700..a7d0aae 100755 --- a/tests/google/appengine/api/mail_unittest.py +++ b/tests/google/appengine/api/mail_unittest.py @@ -32,6 +32,7 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import re +import smtplib import textwrap import zlib @@ -2085,6 +2086,78 @@ def testInvalidAttachmentType(self): body='A body.', attachments=[('virus.exe', b'some data')]) + @mock.patch('smtplib.SMTP') + def testSendAdminEmailViaSmtp_NoRecipients(self, mock_smtp): + """Tests that an error is raised when no admin recipients are specified.""" + environ = { + 'USE_SMTP_MAIL_SERVICE': 'true', + 'SMTP_HOST': 'smtp.example.com', + } + + with mock.patch.dict('os.environ', environ): + with self.assertRaises(mail.MissingRecipientsError): + mail.send_mail_to_admins( + sender='sender@example.com', + subject='Admin Subject', + body='Admin body.') + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_AuthenticationError(self, mock_smtp): + """Tests that an authentication error is handled correctly.""" + environ = { + 'USE_SMTP_MAIL_SERVICE': 'true', + 'SMTP_HOST': 'smtp.example.com', + 'SMTP_USER': 'user', + 'SMTP_PASSWORD': 'password', + } + + instance = mock_smtp.return_value.__enter__.return_value + instance.login.side_effect = smtplib.SMTPAuthenticationError(535, 'Auth failed') + + with mock.patch.dict('os.environ', environ): + with self.assertRaises(mail.InvalidSenderError): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.') + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_ConnectionError(self, mock_smtp): + """Tests that a connection error is handled correctly.""" + environ = { + 'USE_SMTP_MAIL_SERVICE': 'true', + 'SMTP_HOST': 'smtp.example.com', + } + + mock_smtp.side_effect = smtplib.SMTPConnectError(550, 'Connection refused') + + with mock.patch.dict('os.environ', environ): + with self.assertRaises(mail.Error): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.') + + @mock.patch('smtplib.SMTP') + def testSendEmailViaSmtp_ConnectionError(self, mock_smtp): + """Tests that a connection error is handled correctly.""" + environ = { + 'USE_SMTP_MAIL_SERVICE': 'true', + 'SMTP_HOST': 'smtp.example.com', + } + + mock_smtp.side_effect = smtplib.SMTPConnectError(550, 'Connection refused') + + with mock.patch.dict('os.environ', environ): + with self.assertRaises(mail.Error): + mail.send_mail( + sender='sender@example.com', + to='recipient@example.com', + subject='A Subject', + body='A body.') + @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithUnicode(self, mock_smtp): """Tests that unicode characters are handled correctly.""" From a1dd02935d653254ed88d26025286d0685251d3d Mon Sep 17 00:00:00 2001 From: Abhishek Ranjan Date: Fri, 1 Aug 2025 11:49:55 +0530 Subject: [PATCH 6/6] Refactor: Prefix SMTP environment variables with APPENGINE_ This change prefixes all environment variables related to the external SMTP mail service with `APPENGINE_` to improve namespacing and prevent potential conflicts in the deployment environment. The following variables have been renamed: - USE_SMTP_MAIL_SERVICE -> APPENGINE_USE_SMTP_MAIL_SERVICE - SMTP_HOST -> APPENGINE_SMTP_HOST - SMTP_PORT -> APPENGINE_SMTP_PORT - SMTP_USER -> APPENGINE_SMTP_USER - SMTP_PASSWORD -> APPENGINE_SMTP_PASSWORD - SMTP_USE_TLS -> APPENGINE_SMTP_USE_TLS - ADMIN_EMAIL_RECIPIENTS -> APPENGINE_ADMIN_EMAIL_RECIPIENTS The changes have been applied to both the SDK implementation in `mail.py` and the corresponding unit tests in `mail_unittest.py --- src/google/appengine/api/mail.py | 14 ++-- tests/google/appengine/api/mail_unittest.py | 88 ++++++++++----------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/google/appengine/api/mail.py b/src/google/appengine/api/mail.py index 3172477..dfd1fb4 100755 --- a/src/google/appengine/api/mail.py +++ b/src/google/appengine/api/mail.py @@ -1208,7 +1208,7 @@ def send(self, make_sync_call=apiproxy_stub_map.MakeSyncCall): Args: make_sync_call: Method that will make a synchronous call to the API proxy. """ - if os.environ.get('USE_SMTP_MAIL_SERVICE') == 'true': + if os.environ.get('APPENGINE_USE_SMTP_MAIL_SERVICE') == 'true': logging.info('Sending email via SMTP.') mime_message = self.to_mime_message() @@ -1218,7 +1218,7 @@ def send(self, make_sync_call=apiproxy_stub_map.MakeSyncCall): recipients = [] if isinstance(self, AdminEmailMessage): - admin_emails = os.environ.get('ADMIN_EMAIL_RECIPIENTS') + admin_emails = os.environ.get('APPENGINE_ADMIN_EMAIL_RECIPIENTS') if admin_emails: recipients.extend(admin_emails.split(',')) else: @@ -1233,11 +1233,11 @@ def send(self, make_sync_call=apiproxy_stub_map.MakeSyncCall): raise MissingRecipientsError() try: - host = os.environ['SMTP_HOST'] - port = int(os.environ.get('SMTP_PORT', 587)) - user = os.environ.get('SMTP_USER') - password = os.environ.get('SMTP_PASSWORD') - use_tls = os.environ.get('SMTP_USE_TLS', 'true').lower() == 'true' + host = os.environ['APPENGINE_SMTP_HOST'] + port = int(os.environ.get('APPENGINE_SMTP_PORT', 587)) + user = os.environ.get('APPENGINE_SMTP_USER') + password = os.environ.get('APPENGINE_SMTP_PASSWORD') + use_tls = os.environ.get('APPENGINE_SMTP_USE_TLS', 'true').lower() == 'true' with smtplib.SMTP(host, port) as server: if use_tls: diff --git a/tests/google/appengine/api/mail_unittest.py b/tests/google/appengine/api/mail_unittest.py index a7d0aae..5724ec3 100755 --- a/tests/google/appengine/api/mail_unittest.py +++ b/tests/google/appengine/api/mail_unittest.py @@ -1636,12 +1636,12 @@ def FakeMakeSyncCall(service, method, request, response): def testSendEmailViaSmtp(self, mock_smtp): """Tests that mail.send_mail uses SMTP when configured.""" environ = { - 'USE_SMTP_MAIL_SERVICE': 'true', - 'SMTP_HOST': 'smtp.example.com', - 'SMTP_PORT': '587', - 'SMTP_USER': 'user', - 'SMTP_PASSWORD': 'password', - 'SMTP_USE_TLS': 'true', + 'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', + 'APPENGINE_SMTP_HOST': 'smtp.example.com', + 'APPENGINE_SMTP_PORT': '587', + 'APPENGINE_SMTP_USER': 'user', + 'APPENGINE_SMTP_PASSWORD': 'password', + 'APPENGINE_SMTP_USE_TLS': 'true', } with mock.patch.dict('os.environ', environ): @@ -1681,12 +1681,12 @@ def testSendEmailViaSmtp(self, mock_smtp): def testSendEmailViaSmtp_MultipleRecipients(self, mock_smtp): """Tests that mail.send_mail handles multiple recipients via SMTP.""" environ = { - 'USE_SMTP_MAIL_SERVICE': 'true', - 'SMTP_HOST': 'smtp.example.com', - 'SMTP_PORT': '587', - 'SMTP_USER': 'user', - 'SMTP_PASSWORD': 'password', - 'SMTP_USE_TLS': 'true', + 'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', + 'APPENGINE_SMTP_HOST': 'smtp.example.com', + 'APPENGINE_SMTP_PORT': '587', + 'APPENGINE_SMTP_USER': 'user', + 'APPENGINE_SMTP_PASSWORD': 'password', + 'APPENGINE_SMTP_USE_TLS': 'true', } to_list = ['to1@example.com', 'to2@example.com'] @@ -1718,9 +1718,9 @@ def testSendEmailViaSmtp_MultipleRecipients(self, mock_smtp): def testSendEmailViaSmtp_HtmlBody(self, mock_smtp): """Tests that mail.send_mail handles HTML bodies correctly.""" environ = { - 'USE_SMTP_MAIL_SERVICE': 'true', - 'SMTP_HOST': 'smtp.example.com', - 'SMTP_PORT': '587', + 'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', + 'APPENGINE_SMTP_HOST': 'smtp.example.com', + 'APPENGINE_SMTP_PORT': '587', } text_body = 'This is the plain text body.' @@ -1762,7 +1762,7 @@ def testSendEmailViaSmtp_HtmlBody(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_AttachmentsOnly(self, mock_smtp): """Tests sending an email with only attachments.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} attachments = [ ('one.txt', b'data1'), ('two.txt', b'data2'), @@ -1798,7 +1798,7 @@ def testSendEmailViaSmtp_AttachmentsOnly(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_HtmlBodyOnly(self, mock_smtp): """Tests sending an email with only an HTML body.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} html_body = '

Just HTML

' with mock.patch.dict('os.environ', environ): @@ -1835,7 +1835,7 @@ def testSendEmailViaSmtp_HtmlBodyOnly(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithAttachment(self, mock_smtp): """Tests that mail.send_mail handles a single attachment correctly.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} attachment_data = b'This is attachment data.' with mock.patch.dict('os.environ', environ): @@ -1863,7 +1863,7 @@ def testSendEmailViaSmtp_WithAttachment(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithMultipleAttachments(self, mock_smtp): """Tests that mail.send_mail handles multiple attachments correctly.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} attachments = [ ('one.txt', b'data1'), ('two.txt', b'data2'), @@ -1896,7 +1896,7 @@ def testSendEmailViaSmtp_WithMultipleAttachments(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithHtmlAndAttachment(self, mock_smtp): """Tests handling of both HTML body and attachments.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} with mock.patch.dict('os.environ', environ): mail.send_mail( @@ -1928,7 +1928,7 @@ def testSendEmailViaSmtp_WithHtmlAndAttachment(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithReplyTo(self, mock_smtp): """Tests that the Reply-To header is handled correctly.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} with mock.patch.dict('os.environ', environ): mail.send_mail( @@ -1946,7 +1946,7 @@ def testSendEmailViaSmtp_WithReplyTo(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithCustomHeaders(self, mock_smtp): """Tests that custom headers are handled correctly.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} headers = { 'List-Id': 'some-list ', 'References': '' @@ -1969,7 +1969,7 @@ def testSendEmailViaSmtp_WithCustomHeaders(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithAttachmentContentId(self, mock_smtp): """Tests that attachments with Content-ID are handled correctly.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} attachment = mail.Attachment( 'image.png', b'image data', content_id='') @@ -1993,9 +1993,9 @@ def testSendEmailViaSmtp_WithAttachmentContentId(self, mock_smtp): def testSendEmailViaSmtp_NoTls(self, mock_smtp): """Tests that TLS is not used when disabled.""" environ = { - 'USE_SMTP_MAIL_SERVICE': 'true', - 'SMTP_HOST': 'smtp.example.com', - 'SMTP_USE_TLS': 'false', + 'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', + 'APPENGINE_SMTP_HOST': 'smtp.example.com', + 'APPENGINE_SMTP_USE_TLS': 'false', } with mock.patch.dict('os.environ', environ): @@ -2012,8 +2012,8 @@ def testSendEmailViaSmtp_NoTls(self, mock_smtp): def testSendEmailViaSmtp_NoAuth(self, mock_smtp): """Tests that login is not attempted when credentials are not provided.""" environ = { - 'USE_SMTP_MAIL_SERVICE': 'true', - 'SMTP_HOST': 'smtp.example.com', + 'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', + 'APPENGINE_SMTP_HOST': 'smtp.example.com', } with mock.patch.dict('os.environ', environ): @@ -2031,9 +2031,9 @@ def testSendAdminEmailViaSmtp(self, mock_smtp): """Tests that admin emails are sent to the list in the env var.""" admin_list = 'admin1@example.com,admin2@example.com' environ = { - 'USE_SMTP_MAIL_SERVICE': 'true', - 'SMTP_HOST': 'smtp.example.com', - 'ADMIN_EMAIL_RECIPIENTS': admin_list, + 'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', + 'APPENGINE_SMTP_HOST': 'smtp.example.com', + 'APPENGINE_ADMIN_EMAIL_RECIPIENTS': admin_list, } with mock.patch.dict('os.environ', environ): @@ -2049,7 +2049,7 @@ def testSendAdminEmailViaSmtp(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_AmpHtmlBody(self, mock_smtp): """Tests that mail.send_mail handles AMP HTML bodies correctly.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} amp_html_body = 'AMP for Email is awesome!' with mock.patch.dict('os.environ', environ): @@ -2090,8 +2090,8 @@ def testInvalidAttachmentType(self): def testSendAdminEmailViaSmtp_NoRecipients(self, mock_smtp): """Tests that an error is raised when no admin recipients are specified.""" environ = { - 'USE_SMTP_MAIL_SERVICE': 'true', - 'SMTP_HOST': 'smtp.example.com', + 'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', + 'APPENGINE_SMTP_HOST': 'smtp.example.com', } with mock.patch.dict('os.environ', environ): @@ -2105,10 +2105,10 @@ def testSendAdminEmailViaSmtp_NoRecipients(self, mock_smtp): def testSendEmailViaSmtp_AuthenticationError(self, mock_smtp): """Tests that an authentication error is handled correctly.""" environ = { - 'USE_SMTP_MAIL_SERVICE': 'true', - 'SMTP_HOST': 'smtp.example.com', - 'SMTP_USER': 'user', - 'SMTP_PASSWORD': 'password', + 'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', + 'APPENGINE_SMTP_HOST': 'smtp.example.com', + 'APPENGINE_SMTP_USER': 'user', + 'APPENGINE_SMTP_PASSWORD': 'password', } instance = mock_smtp.return_value.__enter__.return_value @@ -2126,8 +2126,8 @@ def testSendEmailViaSmtp_AuthenticationError(self, mock_smtp): def testSendEmailViaSmtp_ConnectionError(self, mock_smtp): """Tests that a connection error is handled correctly.""" environ = { - 'USE_SMTP_MAIL_SERVICE': 'true', - 'SMTP_HOST': 'smtp.example.com', + 'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', + 'APPENGINE_SMTP_HOST': 'smtp.example.com', } mock_smtp.side_effect = smtplib.SMTPConnectError(550, 'Connection refused') @@ -2144,8 +2144,8 @@ def testSendEmailViaSmtp_ConnectionError(self, mock_smtp): def testSendEmailViaSmtp_ConnectionError(self, mock_smtp): """Tests that a connection error is handled correctly.""" environ = { - 'USE_SMTP_MAIL_SERVICE': 'true', - 'SMTP_HOST': 'smtp.example.com', + 'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', + 'APPENGINE_SMTP_HOST': 'smtp.example.com', } mock_smtp.side_effect = smtplib.SMTPConnectError(550, 'Connection refused') @@ -2161,7 +2161,7 @@ def testSendEmailViaSmtp_ConnectionError(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithUnicode(self, mock_smtp): """Tests that unicode characters are handled correctly.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} sender = u'J\xe9r\xe9my ' subject = u'Un sujet avec des caract\xe8res sp\xe9ciaux' @@ -2191,7 +2191,7 @@ def testSendEmailViaSmtp_WithUnicode(self, mock_smtp): @mock.patch('smtplib.SMTP') def testSendEmailViaSmtp_WithAmpHtml(self, mock_smtp): """Tests that AMP HTML is handled correctly.""" - environ = {'USE_SMTP_MAIL_SERVICE': 'true', 'SMTP_HOST': 'smtp.example.com'} + environ = {'APPENGINE_USE_SMTP_MAIL_SERVICE': 'true', 'APPENGINE_SMTP_HOST': 'smtp.example.com'} text_body = 'Plain text' html_body = '

HTML

'