From a271f254516a2587a85db25f6b3044fd66b9d0c2 Mon Sep 17 00:00:00 2001 From: Joshua Laferriere Date: Sat, 24 Feb 2024 17:43:30 -0800 Subject: [PATCH] postfix --- LICENSE | 0 README.md | 0 __pycache__/agent_loader.cpython-311.pyc | Bin 0 -> 3231 bytes __pycache__/agent_operator.cpython-311.pyc | Bin 0 -> 23084 bytes __pycache__/cards.cpython-311.pyc | Bin 0 -> 2556 bytes __pycache__/email_client.cpython-311.pyc | Bin 0 -> 44476 bytes __pycache__/gpt.cpython-311.pyc | Bin 0 -> 10112 bytes __pycache__/pdf2text.cpython-311.pyc | Bin 0 -> 1225 bytes __pycache__/shortcode.cpython-311.pyc | Bin 0 -> 5515 bytes agent_loader.py | 0 agent_operator.py | 0 agents/agents.json | 30 +- agents/new_agent_files/1.txt | 0 agents/new_agent_files/10.txt | 0 agents/new_agent_files/11.txt | 0 agents/new_agent_files/12.txt | 0 agents/new_agent_files/13.txt | 0 agents/new_agent_files/14.txt | 0 agents/new_agent_files/15.txt | 0 agents/new_agent_files/2.txt | 0 agents/new_agent_files/3.txt | 0 agents/new_agent_files/4.txt | 0 agents/new_agent_files/5.txt | 0 agents/new_agent_files/6.txt | 0 agents/new_agent_files/7.txt | 0 agents/new_agent_files/8.txt | 0 agents/new_agent_files/9.txt | 0 .../0bdb90f6-d2c8-408b-abdd-cb724d5ba6ba.png | Bin .../19a4d6db-9148-499e-848a-9c4bcd9b6b38.png | Bin .../1acb636e-2ce8-4d70-8e1d-18609e8626bb.png | Bin .../243455ab-eb20-427a-9343-0a813cb3d759.png | Bin .../37f2c50d-205c-4f65-ae38-f6b8c6565e58.png | Bin .../3ce264f2-8d98-4b10-b65d-0f01ce8fc43e.png | Bin .../451da327-6b04-4f98-8208-4f249ef11aaf.png | Bin .../5a8b5285-68ff-419f-84dc-440774a58cf8.png | Bin .../6b251dd9-b336-4ce4-8002-0c7e079f2bc6.png | Bin .../6eb2b0a6-7a4d-4ada-8399-1b13fdbdb4a2.png | Bin .../7b32abaa-e75b-4f22-bc67-082db025dece.png | Bin .../7f606e2f-4d21-4a9b-bffb-fb59fbc95322.png | Bin .../81770a8a-78fc-40da-a21d-8f49eccf7f35.png | Bin .../977e7696-f898-4da3-b295-88936db01028.png | Bin .../9ce541fa-6ccd-4938-93ad-e29405b520fc.png | Bin .../a6b2d3bf-0daf-4383-8984-ad990429c374.png | Bin agents/pics/cover_photo.png | Bin .../edd4c5d5-d993-4735-984c-f6ee4ad2f96b.png | Bin .../f9d75dff-f3ae-49c7-9d15-9f21acc5f9f4.png | Bin .../ffb03fc9-8ae5-4b08-8a8d-446986030e0a.png | Bin agents/temp_agents.json | 0 api_state.pkl | Bin 0 -> 69 bytes cards.py | 0 contribute.md | 0 email_client-api.py | 874 ++++++++++++++++++ email_client.log | 242 +++++ email_client.py | 418 +++++---- error-response-email.txt | 0 gpt.py | 2 + instructions.json | 0 main.py | 4 + pdf2text.py | 0 processed_threads.json | 2 +- shortcode.py | 0 start-config.json | 0 start.py | 4 + static/content.json | 0 static/favicon.ico | Bin static/logo.png | Bin static/logos/atat-board-art.jpg | Bin static/logos/atat-board-basic.png | Bin static/logos/atat-board.png | Bin static/logos/atat-glyph.png | Bin static/logos/atat-wide-trans.png | Bin static/logos/atat-wide-white-trans.png | Bin static/logos/atat.png | Bin static/logos/email-example-1.png | Bin static/logos/email-example-2.png | Bin static/logos/sl-logo-art-square.png | Bin static/logos/sl-logo-black-square.png | Bin static/logos/sl-logo-white-wide-trans.png | Bin static/logos/sl-logo-wide-black.png | Bin static/logos/sl-logo-wide-trans.png | Bin static/logos/sl-trans-glyph.png | Bin static/main.css | 0 templates/index.html | 0 templates/readme.html | 0 test_email.py | 40 + thread-reconciler.py | 5 + tools/__init__.py | 0 tools/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 137 bytes tools/__pycache__/update_team.cpython-311.pyc | Bin 0 -> 7632 bytes tools/render_agents.py | 1 + tools/testing_emails.md | 0 tools/thread-reconciler.py | 5 + tools/tuple-finder.py | 0 tools/update_content.py | 6 + tools/update_team.py | 4 + tools/vars_tester.py | 0 96 files changed, 1421 insertions(+), 216 deletions(-) mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README.md create mode 100755 __pycache__/agent_loader.cpython-311.pyc create mode 100755 __pycache__/agent_operator.cpython-311.pyc create mode 100755 __pycache__/cards.cpython-311.pyc create mode 100644 __pycache__/email_client.cpython-311.pyc create mode 100755 __pycache__/gpt.cpython-311.pyc create mode 100755 __pycache__/pdf2text.cpython-311.pyc create mode 100755 __pycache__/shortcode.cpython-311.pyc mode change 100644 => 100755 agent_loader.py mode change 100644 => 100755 agent_operator.py mode change 100644 => 100755 agents/agents.json mode change 100644 => 100755 agents/new_agent_files/1.txt mode change 100644 => 100755 agents/new_agent_files/10.txt mode change 100644 => 100755 agents/new_agent_files/11.txt mode change 100644 => 100755 agents/new_agent_files/12.txt mode change 100644 => 100755 agents/new_agent_files/13.txt mode change 100644 => 100755 agents/new_agent_files/14.txt mode change 100644 => 100755 agents/new_agent_files/15.txt mode change 100644 => 100755 agents/new_agent_files/2.txt mode change 100644 => 100755 agents/new_agent_files/3.txt mode change 100644 => 100755 agents/new_agent_files/4.txt mode change 100644 => 100755 agents/new_agent_files/5.txt mode change 100644 => 100755 agents/new_agent_files/6.txt mode change 100644 => 100755 agents/new_agent_files/7.txt mode change 100644 => 100755 agents/new_agent_files/8.txt mode change 100644 => 100755 agents/new_agent_files/9.txt mode change 100644 => 100755 agents/pics/0bdb90f6-d2c8-408b-abdd-cb724d5ba6ba.png mode change 100644 => 100755 agents/pics/19a4d6db-9148-499e-848a-9c4bcd9b6b38.png mode change 100644 => 100755 agents/pics/1acb636e-2ce8-4d70-8e1d-18609e8626bb.png mode change 100644 => 100755 agents/pics/243455ab-eb20-427a-9343-0a813cb3d759.png mode change 100644 => 100755 agents/pics/37f2c50d-205c-4f65-ae38-f6b8c6565e58.png mode change 100644 => 100755 agents/pics/3ce264f2-8d98-4b10-b65d-0f01ce8fc43e.png mode change 100644 => 100755 agents/pics/451da327-6b04-4f98-8208-4f249ef11aaf.png mode change 100644 => 100755 agents/pics/5a8b5285-68ff-419f-84dc-440774a58cf8.png mode change 100644 => 100755 agents/pics/6b251dd9-b336-4ce4-8002-0c7e079f2bc6.png mode change 100644 => 100755 agents/pics/6eb2b0a6-7a4d-4ada-8399-1b13fdbdb4a2.png mode change 100644 => 100755 agents/pics/7b32abaa-e75b-4f22-bc67-082db025dece.png mode change 100644 => 100755 agents/pics/7f606e2f-4d21-4a9b-bffb-fb59fbc95322.png mode change 100644 => 100755 agents/pics/81770a8a-78fc-40da-a21d-8f49eccf7f35.png mode change 100644 => 100755 agents/pics/977e7696-f898-4da3-b295-88936db01028.png mode change 100644 => 100755 agents/pics/9ce541fa-6ccd-4938-93ad-e29405b520fc.png mode change 100644 => 100755 agents/pics/a6b2d3bf-0daf-4383-8984-ad990429c374.png mode change 100644 => 100755 agents/pics/cover_photo.png mode change 100644 => 100755 agents/pics/edd4c5d5-d993-4735-984c-f6ee4ad2f96b.png mode change 100644 => 100755 agents/pics/f9d75dff-f3ae-49c7-9d15-9f21acc5f9f4.png mode change 100644 => 100755 agents/pics/ffb03fc9-8ae5-4b08-8a8d-446986030e0a.png mode change 100644 => 100755 agents/temp_agents.json create mode 100755 api_state.pkl mode change 100644 => 100755 cards.py mode change 100644 => 100755 contribute.md create mode 100755 email_client-api.py create mode 100755 email_client.log mode change 100644 => 100755 email_client.py mode change 100644 => 100755 error-response-email.txt mode change 100644 => 100755 gpt.py mode change 100644 => 100755 instructions.json mode change 100644 => 100755 main.py mode change 100644 => 100755 pdf2text.py mode change 100644 => 100755 shortcode.py mode change 100644 => 100755 start-config.json mode change 100644 => 100755 start.py mode change 100644 => 100755 static/content.json mode change 100644 => 100755 static/favicon.ico mode change 100644 => 100755 static/logo.png mode change 100644 => 100755 static/logos/atat-board-art.jpg mode change 100644 => 100755 static/logos/atat-board-basic.png mode change 100644 => 100755 static/logos/atat-board.png mode change 100644 => 100755 static/logos/atat-glyph.png mode change 100644 => 100755 static/logos/atat-wide-trans.png mode change 100644 => 100755 static/logos/atat-wide-white-trans.png mode change 100644 => 100755 static/logos/atat.png mode change 100644 => 100755 static/logos/email-example-1.png mode change 100644 => 100755 static/logos/email-example-2.png mode change 100644 => 100755 static/logos/sl-logo-art-square.png mode change 100644 => 100755 static/logos/sl-logo-black-square.png mode change 100644 => 100755 static/logos/sl-logo-white-wide-trans.png mode change 100644 => 100755 static/logos/sl-logo-wide-black.png mode change 100644 => 100755 static/logos/sl-logo-wide-trans.png mode change 100644 => 100755 static/logos/sl-trans-glyph.png mode change 100644 => 100755 static/main.css mode change 100644 => 100755 templates/index.html mode change 100644 => 100755 templates/readme.html create mode 100755 test_email.py mode change 100644 => 100755 thread-reconciler.py mode change 100644 => 100755 tools/__init__.py create mode 100755 tools/__pycache__/__init__.cpython-311.pyc create mode 100755 tools/__pycache__/update_team.cpython-311.pyc mode change 100644 => 100755 tools/render_agents.py mode change 100644 => 100755 tools/testing_emails.md mode change 100644 => 100755 tools/thread-reconciler.py mode change 100644 => 100755 tools/tuple-finder.py mode change 100644 => 100755 tools/update_content.py mode change 100644 => 100755 tools/update_team.py mode change 100644 => 100755 tools/vars_tester.py diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/__pycache__/agent_loader.cpython-311.pyc b/__pycache__/agent_loader.cpython-311.pyc new file mode 100755 index 0000000000000000000000000000000000000000..558083c9ddcb20ff59277572906094b12de8c985 GIT binary patch literal 3231 zcma)8TW=dh6rR2Lwsw3W4RPJ1ZsOE+h#Xo9s?rEGq67hnUy_$bpp}!|q;8yby6e!^ ztdSog;YOAcs!B0XgaJ{g`odr6Kd>!}n3XC;LVe)PRr*l%i8Hgd>y1ey|xCAPo{svjVS>b|}`-%!;zRPlvszEJM+=BGJ$4)6w#KHYif*+m+X2?9!uRTQrbARAjj7vHFLtW{X@NEb|1`x zQ}FJ>#XUYy`$%oo0_*aBr+puyly4knV>1fJq`o?&a5f>%--im$Cg3z$Ir0BI)*x4y z-IitKS<}2WF<|L-G4mhj@y=&3F3W;mw$!<^SIp3;*BP}+bStdJWEr3nRXDFPtyp6X zE-(-`I+sXGYpTvAQ#T}~MHL^Y)_a!XswAkAtR~WlFC?WO3t19>X1eJ1b^vDjOr&Yc zdtt76696dV^yEIQTaPu6h4a*Ve1+BR)K;pvSPE5o!nK}oPRdF9{(*aeJ^yIcKU(pR z)%;`NW2=Y2?YHuw9S-7pdtz(i_GDqQc<#pw-(C22;@(7+A1U)Ab>Ex7F^4-p(%_Ku zG-iS~H)8~Sr$6u+h~KB7vWq$c9x`-0*rBssv_px&9PAL7ROY=8u1ZU*!NhVfeN_ss zfZgPvv=&b!gNsr!C9kN#bSjvVrC`b!VT4~4QaihtN+oq)+hoGxErHC0)QIC(=SWM% zXK2Q$-jh(9X_5400w(L5JA*J!9R$dsC%k)y-SdR2p78Hn#S^W0q7{C;#*dfTal=rZ zG;ABH0F!ZoFTuOJKz^eShl9o_{Tg$$F~#oifh;+P(pX=!h_pt14;T6mjM>x_gekt# zW4d)+QXu!mGvUk9;>OBE&}4y^#%vBd6VCX;k>GM_Ll%Q_DoD&9jWEhd(5M^-(7AOb zA*VYdK^efEnEuLX02OZ#=Sm}4>fSCbnZf_wMF_7l4?v<}JGK=oo-2M;aYk#-D2@#$ zjtyVkb8Ods$6kz-<}04jnrAe(vhV86$xkeN**WvaB7pI$SY~UM*|KG}?(*j6yP1xi zNtpg8RKS5$3Eti1@HN+YSsm>{b0*8uko(#Z_BuHVa^ESE`xv+&UqKVI4vlW-HFk-n zfCPmhLIo!Fg1V$&?GaYz)b(T{t(<@s$#hBp%bVBWFw}KNGPM*>3hQwtt=k9*goH?3 ze8{`X5Dd?pe1$(-AsU52>KXtz)Z4ckxDzPORlKKa-cu!j(4Kd+>K(0k$7c^bC3=JZoBigosqiTyFIuySh4$Sc7Ji9Y7dv~;kvsw@5E1o zL5?%~ygPrMcsdrLrXvpgWrs7fAm|Q3SWAf;NnCdd!u5@K(&$m}y{&jK;QQU6Jf@<8 zzoW@PA0V=(@e#mcDF*ykzXr&)p9V`YBXFO$z(O(jj&Fg*!uWv0E$}!PZv&a3pJv{M zGq=EI!-LJx%Q2AiyTRBa-W8-qe6x^UDra#Gf7_AmzpzajW_VvY2Qr+`)k^^Iu|ZLF X#OKJ7Owb`0URR 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 B8TSAH literal 0 HcmV?d00001 diff --git a/__pycache__/email_client.cpython-311.pyc b/__pycache__/email_client.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27000eaaed0692aad09875529b45b8756edb2cfb GIT binary patch literal 44476 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 literal 0 HcmV?d00001 diff --git a/__pycache__/gpt.cpython-311.pyc b/__pycache__/gpt.cpython-311.pyc new file mode 100755 index 0000000000000000000000000000000000000000..91d8580273060d0f310fb031da440d8ca026c1da GIT binary patch literal 10112 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 Lzj;Ng# literal 0 HcmV?d00001 diff --git a/__pycache__/pdf2text.cpython-311.pyc b/__pycache__/pdf2text.cpython-311.pyc new file mode 100755 index 0000000000000000000000000000000000000000..c2a814a56955dc864c8fc7ccd6273e4b14d2071f GIT binary patch literal 1225 zcmZWo&rcIU6n@j~mTjd-EG<6*&BPyQNu(C_0!C8;1}~K;7f1|Qx>E#UTW6mcrqTmH6bR3lW&%`6nxWt`@J{s`}WOC=WQ?;0JOa) z-`S!9@KXj&^EAN0Ac9w5fI-$^;pj~kh~e3R1&=`4=TdJBxaFK zO2iMMtzxx>i?E0IeJd_`gk(2tRjRmF^qi_wgul55AkG})qO$61MkhHZqbYbaT=KS8 z7rp&WB9zo3DXNu@R-z5;A%5SAw}JpA^5U{NasfnlD-9Qc50!i(beu6fMc*6PJ#`WS z^I#|?-*ca#JZzv1vH=BU9yUl(&e*&4F+kC#3xexuVNt;ff@XMjo$7RXS?9^Sl zv?&-hbfJ$1-11qS>+4J~ZjHOKl*k#GMOi|x$Z9J#PUjx8V(28p2;N5rqg-Ok$#n? zVgbjql65*JWq}JgI&l;Hr*bL*2%5`&@Im#L{9W|#fGt230w6G{gP+QstBh&3s_Xy(@)uS5=Emi#}+F`^wt z;&PDj(>QZ#z}@cBwtT!o4pSDq1xh zv%|4+;ExX?gLe2*Wwjbk*x`hm>5uNFDvdh(ADx(t#jbD@huwv8jV+@2|Jpob|%WHa;mN(;qM;}I>Sg)dyd@?Ex6w`{^2P) z{8hCHTbtO|CTeFxWzk%J(NIC{k#E!(*!T8ZeR!Om4&WUeAOs#kcc|T3&l>qPcAF1iC~v=Y4P9L!gw(*l?XDUGx7}6u4v~R6 K{O`EDu>SxhaW8cM literal 0 HcmV?d00001 diff --git a/__pycache__/shortcode.cpython-311.pyc b/__pycache__/shortcode.cpython-311.pyc new file mode 100755 index 0000000000000000000000000000000000000000..40099d5594c40735a8557b1dedf5354f6e22e5eb GIT binary patch literal 5515 zcmb^#TWl29_0H}*_TgRZ^<%v@ws(_YdkwpW*ntEvu7O}^ut~60H3Yg^cEDJI$g0)D*b>QI!=EEp?t7K)Z3r3r_ zNY)e~5zzNoYAh*P-?jLv)7SA!06(xES&vby(W*^?%5`{e)H?Do@8-Lb1!PO*e(dl= znaU*MqF&xl%|-cWOyqbN%ECZ2#riYQHOogcbN|r`gQRXva4DURW-?rg)hT`^jz5CP$1^a>n!ncl3? z&JvB9+ON1iaxJmNv0_eXd{%9Iwr~WxyZe@(`XsUxS+pVqEv0Oul#@ZDHg%LP#D!F8%O8#pN3n%j1qOQz3o{!}^@8k;(Bl=9)0@?;IFChZ>k`7SSUeuy zn@Go^31MHjhO`Cd2-^Vs3w{M8Hx9wG`1%*I<-lh%*JhT5&lAPOVp9h%uvQFfA+l2b`R_9A>j{Fx`pQ;;NeilH$W$kBNC6# z(ICx(M3C-IM$biz_-6!HeiQDr3lM<0RE!gJS1il(&;lHYi@L2gLIWZMMJR@!=sr{U zi^Nk95zDcZ4+3gGyu#lA6p-e06?*Tw8!yc*zF9o`W&ZR0XK!75OL6Z~-Mb1w&C^l{ zX`ap!qO5%eD9qi4XgQ)VK9%u(dZ1|gc;f1W!UUJc6ee`DdxaU5nNf{yUa~IDee7QK zhE}{G#XG2a2Nim-kSrwcRATGIYD?crOP|sbR9k`yJ)qJ9GCiQRbzez+lv3Jy)wW&( zo${0rVGU}e!-%M%RnL|c2&bMt)zhbt{VLfnll_`~Qz2ydlp*t2y;Wh^7!mU_^j<;r z{lvQ%g5_z{tu4@`@`N1{ZTGOz* zreUo0wvD=8^IFKlypuU9E6#+T*Xj6dD$R2_=bV^KgdwjMI6i3AN%4G!D<2Wg6n8dK zNo#Zd0LzKdcp@T*QCtWmz82g_U4Z{LVex#WOyH1E^VDczkyX_Qzw;b8*)PBkzy=eDu<24P)(SfrOBnNknG_ zoi59s$N8n98y+v4I~+JE3<%j|GRntu+{rMs`cF=69SuUItS6`h`d>RKoSZs^^<_dg zaMBr!k_$~$S9z5B{Ac6hoIjanqmWOW-|aWb6-y9JilgEy~cwlVm#d3CpFK)q2 zx{FIVp=Ezom@UAFL5%Pf%hqYAWfP+A zuDDDxD#qp@r}C8B99u&bbjld4p;pPd%UM;`BK0Qhi)PN6YO;(}2=LG0UDowiS8J)E z){z!)e8PELT`8w@9gnwj{uoTcF-G`1cu4_$*MORzE{-Uk9ja$XA@m)MntgBuEhZJu zu<99J_3T;k?721mm!B%0N!2r1#^_vv2|?8pEI0i{*K%I*jHsTGRnMUn&mqNgSoIt( zgy8<#>MKs)3dzpH@DzGs`IffJyXEdNt$XXQkAL;*eJj!O{I>`|A$*5wE+MbgbDOlw zj=Fr!*C~^2T2oI6IjKpa3@=ieck`7uK6>NB{N;Qh1%w=3MYn7Z!=w57R(*pjzCn5W z0d4yMAkpz60W<(!bbn_%2Qg%vvZruZcK2xQ!PWM`mG(iUeTUkH4$jp}8T_EN$rE9<1wO^(?;JE|Kb!~^r-S&>F zv}Su=vpFutC<#7lP4=ev&pe2c;NHq_DcN$bbntKO0w%gBM+0#zAptIEiSyLKyJ z17>Lf^UdDk@GX~YKZGC4p~`if$N%8L+0cVOmPsSc4?2u!!|#MXoy13`5s`H3X<^t1 zOUC~`Mj$h;J%*p~gMeL*=f+CZ`03xIqbpQYW literal 0 HcmV?d00001 diff --git a/cards.py b/cards.py old mode 100644 new mode 100755 diff --git a/contribute.md b/contribute.md old mode 100644 new mode 100755 diff --git a/email_client-api.py b/email_client-api.py new file mode 100755 index 00000000..5826d4e5 --- /dev/null +++ b/email_client-api.py @@ -0,0 +1,874 @@ +import email +import html +import imaplib +import json +import logging +import os +import re +import smtplib +import quopri +import tempfile +import shutil +import requests +from time import sleep +from datetime import datetime +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.utils import getaddresses +from contextlib import contextmanager + +from pdf2text import extract_pdf_text +from shortcode import handle_document_short_code +from agent_operator import AgentSelector + +from dotenv import load_dotenv +import os + +load_dotenv() + + +class EmailClient: + + # INIT & SETUP EMAIL ACCESS + + def __init__(self, agent_loader, gpt, testing=False): + logging.basicConfig(filename='email_client.log', + level=logging.INFO, + format='%(asctime)s [%(levelname)s]: %(message)s') + self.agent_loader = agent_loader + self.gpt = gpt + self.testing = testing + self.setup_email_client() + self.processed_threads = self.load_processed_threads() + self.agent_operator = AgentSelector() + self.conversation_threads = {} + self.openai_api_key = os.getenv('OPENAI_API_KEY') + + def get_response_from_api(self, content): + print("api") + + """Get response from external API for the given content.""" + url = "http://127.0.0.1:5000/v1/chat/completions" + headers = {"Content-Type": "application/json"} + data = { + "mode": "chat", + "character": "Example", + "messages": [{"role": "user", "content": content}] + } + response = requests.post(url, headers=headers, json=data, verify=False) + if response.status_code == 200: + api_response = response.json() + assistant_message = api_response['choices'][0]['message']['content'] + return assistant_message + else: + print("Failed to get response from API") + return None + + def setup_email_client(self): + self.smtp_server = os.getenv('SMTP_SERVER') + self.smtp_port = os.getenv('SMTP_PORT') + self.smtp_username = os.getenv('SMTP_USERNAME') + self.smtp_password = os.getenv('SMTP_PASSWORD') + self.connect_to_imap_server() + + def connect_to_imap_server(self): + self.imap_server = imaplib.IMAP4_SSL(os.getenv('IMAP_SERVER')) + self.imap_server.login(self.smtp_username, self.smtp_password) + self.imap_server.debug = 4 + + def check_imap_connection(self): + try: + response = self.imap_server.noop() + status = response[0] + return status == 'OK' + except: + return False + + def start(self): + print("Started email server") + restart_counter = 0 + MAX_RESTARTS = 500 + sleep_time = 6 # Sleep time in seconds + + while True: + try: + self.run_server_loop(sleep_time) + except imaplib.IMAP4.abort as e: + print(f"IMAP connection aborted: {e}. Reconnecting...") + self.restart_system() + restart_counter += 1 + if restart_counter >= MAX_RESTARTS: + print("Max restarts reached. Exiting.") + break + except AssertionError as e: + print(f"Assertion error: {e}. Restarting system...") + self.restart_system() + except Exception as outer_exception: + print(f"Outer exception occurred: {outer_exception}") + self.restart_system() + + def run_server_loop(self, sleep_time): + while True: + if not self.check_imap_connection(): + print("IMAP connection lost. Reconnecting...") + self.connect_to_imap_server() + + self.process_thread() + + print( + f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Sleeping for {sleep_time} seconds." + ) + + for i in range( + sleep_time, 0, + -3): # Decreasing sleep time by 10 seconds in each iteration + print(f"Sleeping... {i} seconds remaining.") + sleep(3) # Sleep for 10 seconds + + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print(f"[{timestamp}] Resuming processing.") + + @contextmanager + def smtp_connection(self): + server = smtplib.SMTP(self.smtp_server, self.smtp_port) + server.starttls() + server.login(self.smtp_username, self.smtp_password) + try: + yield server + except Exception as e: + logging.error(f"Exception occurred in smtp_connection: {e}") + finally: + server.quit() + + def restart_system(self): + print("Restarting system...") + self.connect_to_imap_server() + + # HANDLE THREAD PROCESSING + + def save_processed_threads(self): + file_path = "processed_threads.json" + with tempfile.NamedTemporaryFile('w', + dir=os.path.dirname(file_path), + delete=False) as tmp_file: + json.dump(self.processed_threads, tmp_file) + tmp_file.flush() + shutil.move(tmp_file.name, file_path) + + def load_processed_threads(self): + file_path = "processed_threads.json" + if os.path.exists(file_path): + with open(file_path, 'r') as file: + try: + threads = json.load(file) + #print(f"Debug: Loaded processed_threads: {threads}") + + self.validate_processed_threads(threads) + return threads + except json.JSONDecodeError: + print( + "Error decoding processed_threads.json. Returning an empty list." + ) + return {} + + def validate_processed_threads(self, threads): + if not isinstance(threads, dict): + raise ValueError("Processed threads must be a dictionary.") + for x_gm_thrid, data in threads.items(): + if 'nums' not in data or not (isinstance(data['nums'], list) + or isinstance(data['nums'], dict)): + raise ValueError(f"Invalid 'nums' field for {x_gm_thrid}.") + if 'metadata' not in data or not isinstance(data['metadata'], dict): + raise ValueError(f"Invalid 'metadata' field for {x_gm_thrid}.") + + def update_processed_threads(self, message_id, x_gm_thrid, num, subject, + in_reply_to, references, sender, receiver): + timestamp = datetime.now() + num_str = str(num) + + # Convert receiver to a list if it's not already + if isinstance(receiver, str): + receiver = [receiver] + elif not isinstance(receiver, list): + receiver = list(receiver) # Convert to list if it's another iterable + + # Initialize the thread record if it doesn't exist + if x_gm_thrid not in self.processed_threads: + self.processed_threads[x_gm_thrid] = {'nums': {}, 'metadata': {}} + + # Add the new UID under the existing thread ID + self.processed_threads[x_gm_thrid]['nums'][num_str] = {'processed': True} + + # Update metadata + self.processed_threads[x_gm_thrid]['metadata'] = { + 'subject': subject, + 'timestamp': timestamp.strftime('%Y-%m-%d %H:%M:%S'), + 'sender': sender, + 'receiver': ','.join(receiver), # Joining the list of emails + 'in_reply_to': in_reply_to, + 'references': references + } + self.save_processed_threads() + + # PROCESS EMAILS + + def is_email_processed(self, x_gm_thrid, num): + num_str = str(num) + if x_gm_thrid in self.processed_threads: + if num_str in self.processed_threads[x_gm_thrid]['nums']: + if self.processed_threads[x_gm_thrid]['nums'][num_str].get( + 'processed', False): + print( + f"Skipping already processed message with UID {num} in thread {x_gm_thrid}." + ) + return True + print( + f"is_email_processed allowing response of UID {num} in thread {x_gm_thrid}." + ) + return False + + def load_email(self, num): + """ + Load and parse an email by its unique ID. + + Parameters: + num (str): The unique ID of the email to load. + + Returns: + tuple: A tuple containing parsed email components or None values on failure. + """ + try: + # Standardize the num to string type + num_str = num.decode('utf-8') if isinstance(num, bytes) else str(num) + + # Initialize the return values + from_, to_emails, cc_emails, subject, message_id, in_reply_to, references, content = (None,) * 8 + + # Fetch the email content by UID + result, data = self.imap_server.uid('fetch', num_str, '(RFC822)') + if result != 'OK': + print(f"Error fetching email content for UID {num}: {result}") + return None, None, None, None, None, None, None, None + + raw_email = None + for response_part in data: + if isinstance(response_part, tuple): + raw_email = response_part[1].decode("utf-8") + break + + if not raw_email: + print(f"No email content found for UID {num}") + return None, None, None, None, None, None, None, None + + email_message = email.message_from_string(raw_email) + + # Extract headers + from_ = email_message['From'] + to_emails = [addr[1] for addr in getaddresses([email_message['To']])] + cc_emails = [addr[1] for addr in getaddresses([email_message.get('Cc', '')])] + subject = email_message['Subject'] + message_id = email_message['Message-ID'] + in_reply_to = email_message.get('In-Reply-To', '') + references = email_message.get('References', '') + + # Extract body and attachments + content, pdf_attachments = self.extract_email_content(email_message) + print("load_email") + print(content) + # Append PDF contents to the email content + for pdf_attachment in pdf_attachments: + pdf_label = f"PDF: {pdf_attachment['filename']}" + content += f"\n\n{pdf_label}\n{pdf_attachment['text']}\n{pdf_label}\n" + except: + print("load email except") + + return message_id, num, subject, content, from_, to_emails, cc_emails, references, in_reply_to + + def process_thread(self): + self.imap_server.select("INBOX") + result, data = self.imap_server.uid('search', None, 'UNSEEN') + if result != 'OK': + print(f"Error searching for emails: {result}") + return + + unseen_emails = data[0].split() + thread_to_unseen = {} + + # Group unseen emails by subject and sender as a proxy for thread grouping + for num in unseen_emails: + print(num) + email_data = self.load_email(num) + if email_data is None or len(email_data) < 8: # Adjusted for the updated return values from load_email + continue + message_id, num_str, subject, content, from_, to_emails, cc_emails, references, in_reply_to = email_data + print("process_thread") + print(content) + # Use a combination of subject and sender for grouping + thread_key = f"{subject}:{from_}" + + if thread_key not in thread_to_unseen: + thread_to_unseen[thread_key] = [] + + thread_to_unseen[thread_key].append({ + "message_id": message_id, + "num": num, + "subject": subject, + "from_": from_ + }) + + # Process the most recent unseen email in each pseudo-thread + for thread_key, unseen_list in thread_to_unseen.items(): + unseen_list.sort(key=lambda x: int(x['num']), reverse=True) + most_recent_unseen = unseen_list[0] + if most_recent_unseen['from_'] == self.smtp_username: + continue + + processed = self.process_single_thread(most_recent_unseen['num']) + if not processed: + print(f"Failed to process thread: {thread_key}") + else: + # Mark all other unseen emails in the same pseudo-thread as seen + for unseen_email in unseen_list[1:]: + self.mark_as_seen(unseen_email['num']) + # Update processed threads without x_gm_thrid, using message_id instead + self.update_processed_threads(unseen_email['message_id'], + "", # Empty placeholder for x_gm_thrid + unseen_email['num'], + unseen_email['subject'], "", "", + unseen_email['from_'], "") + + def process_single_thread(self, num): + processed = False + try: + message_id, num_str, subject, content, from_, to_emails, cc_emails, references, in_reply_to = self.load_email(num) + if not message_id: + return processed # Skip if email couldn't be processed + + print("process_single_thread") + print(content) + + # Here's the interception and modification of content + modified_content = self.get_response_from_api(content) + if modified_content: + content = modified_content # Replace original content with modified content + + # Proceed with handling the email, possibly using modified content + successful = self.handle_incoming_email(from_, to_emails, cc_emails, content, subject, message_id, references, num_str, to_emails, cc_emails, content) + if successful: + self.mark_as_seen(num_str) + processed = True + + return processed + except Exception as e: + print(f"Exception while processing emails: {e}") + return False + + def mark_as_seen(self, num): + try: + # Ensure num is decoded to a string if it's bytes + num_str = num.decode('utf-8') if isinstance(num, bytes) else str(num) + + #print( f"Debug: Marking email as seen with UID: {num_str}, Type: {type(num_str)}" ) + + print( + f"Debug: IMAP Server state before STORE command: {self.imap_server.state}" + ) + # Mark the email as seen + result, _ = self.imap_server.uid('store', num_str, '+FLAGS', '(\Seen)') + if result != 'OK': + raise Exception( + f"Failed to mark email as seen. IMAP server returned: {result}") + except Exception as e: + print(f"Exception while marking email as seen with UID {num}: {e}") + import traceback + print(traceback.format_exc()) + + def send_error_email(self, to_email, original_subject, error_reason): + error_file_path = "error-response-email.txt" + if os.path.exists(error_file_path): + with open(error_file_path, 'r') as file: + error_content = file.read().replace("{error_reason}", error_reason) + else: + error_content = f"Thank you for using Semantic Life. Your email with the subject '{original_subject}' could not be processed because {error_reason}. Please try sending your email again. It may be that your email included an attachment, or that your email text was too long. If you did not get the agent you are looking for, email agent@semantic-life.com to ask for help. To customize your personal agent, email atlas@semantic-life.com. For sales inquiries about highly personalized synthetic copies of yourself and others, please email sean@semantic-life.com." + + subject = f"Please try again. || Error: '{original_subject}'" + with self.smtp_connection() as server: + msg = MIMEText(error_content, 'plain') + msg['From'] = self.smtp_username + msg['To'] = to_email + msg['Subject'] = subject + server.sendmail(self.smtp_username, [to_email], msg.as_string()) + + def strip_html_tags(self, text): + clean = re.compile('<.*?>') + return re.sub(clean, '', html.unescape(text)) + + def handle_incoming_email(self, from_, to_emails, cc_emails, thread_content, + subject, message_id, references, num, + initial_to_emails, initial_cc_emails, + original_content): + + print( + f"Debug: Initial thread_content in handle_incoming_email: {thread_content[:200]}..." + ) + + try: + print( + f"Handling incoming email for thread with subject: {subject} and message_id: {message_id}" + ) + shortcode_type = None + print("Entered handle_incoming_email") + #print(f"Debug: Email content at start of handle_incoming_email:{thread_content[:50]}") + + new_content = thread_content + #print("new content",new_content) + # Reset conversation history for a new email thread + self.agent_operator.reset_for_new_thread() + #print("Before human_threads initialization:", from_, to_emails,cc_emails, subject, message_id, references, num) + human_threads = set() + if from_ == self.smtp_username: + print("Ignoring self-sent email.") + print("Thread Content:", thread_content) + return False + print(f"Handling email from: {from_}") + print(f"To emails: {to_emails}") + print(f"CC emails: {cc_emails}") + + thread_content = self.strip_html_tags(thread_content) + print( + f"Handling shortcode for email with subject '{subject}' and content: {thread_content[:242]}..." + ) + + result = handle_document_short_code( + thread_content, self.agent_operator.openai_api_key, + self.agent_operator.conversation_history) + + if result is None: + print( + "Error: email server - handle_document_short_code returned None.") + return False + structured_response = result.get('structured_response') + + # Replace the shortcodes to prevent them from being processed again + thread_content = re.sub(r'!\w+\(.*?\)', '', thread_content) + + if structured_response is not None: + shortcode_type = structured_response.get("type") + + if shortcode_type in ["style", "detail"]: + structured_response = result.get('content', None) + new_content = result.get('new_content', thread_content) + + if shortcode_type == "detail": + # Stitch the detailed responses together + stitched_response = "\n\n".join(structured_response) + # Use the stitched response as the thread_content + thread_content = stitched_response + + elif shortcode_type is not None: + print("Unhandled response_data type.") + + style_info = structured_response.get('structured_response', + '') if structured_response else '' + print("Structured response generated: ") + self.agent_operator.conversation_history += f"\nStructured Response: {structured_response}" # Update conversation history + thread_content = new_content + + recipient_emails = to_emails + cc_emails + + thread_content = original_content + + agents = self.agent_operator.get_agent_names_from_content_and_emails( + thread_content, recipient_emails, self.agent_loader, self.gpt) + + #print(f"Raw agents list before filtering: {agents}") + + # Filter out invalid agent info and ensure we unpack the expected format + agents = [ + agent_info for agent_info in agents + if isinstance(agent_info, tuple) and len(agent_info) == 2 + ] + #print(f"Filtered agents: {agents}") + if not agents: + logging.warning("No valid agent info found") + return False + #print("There are valid agents to process.") + print(f"Identified agents: {agents}") + + # Check if this thread has been processed before + if message_id in self.processed_threads: + print( + f"Email with message_id {message_id} has already been processed.") + return False + + # Prevent agents from responding to other agents + if from_ in [ + agent["email"] for agent in self.agent_loader.agents.values() + ]: + print("Ignoring email from another agent.") + return False + + all_responses_successful = True + previous_responses = [] + + # Ensure proper unpacking for the load_email function + message_id, num_str, subject, content, from_, to_emails, cc_emails, references, in_reply_to = self.load_email( + num) + print("handle_incoming_email") + print(content) + + missing_values = [ + var_name for var_name, value in locals().items() + if value is None and var_name in [ + "message_id", "num", "subject", "content", "from_", "to_emails", + "cc_emails", "references" + ] + ] + + if missing_values: + print( + f"Error processing email with UID {num}. Missing or None values: {', '.join(missing_values)}. Skipping this email." + ) + return False + + # print(f"Unpacking agents: {agents}") + + for agent_info in agents: + if len(agent_info) != 2: + logging.error( + f"Unexpected agent info format (length {len(agent_info)}): {agent_info}" + ) + continue + agent_name, order = agent_info + + # Reset the recipient list to the initial recipient list before processing this agent's response + to_emails = list(initial_to_emails) + cc_emails = list(initial_cc_emails) + + # Generate response + if order == len(agents): + # This is the last agent, append style info to the prompt + response = self.agent_operator.get_response_for_agent( + self.agent_loader, + self.gpt, + agent_name, + order, + agents, + thread_content, + additional_context=f"Note: {style_info}") + else: + response = self.agent_operator.get_response_for_agent( + self.agent_loader, self.gpt, agent_name, order, agents, + thread_content) + + if not response: # Skip empty responses + all_responses_successful = False + continue + + # If the previous message in the thread was from an agent, skip sending the response + if previous_responses and isinstance( + previous_responses[-1], dict) and 'from_' in previous_responses[ + -1] and previous_responses[-1]['from_'] in [ + agent["email"] + for agent in self.agent_loader.agents.values() + ]: + + # Check for explicit tags or '@@' shortcode in the content + if "@@" not in thread_content and not any( + f"@@({name})" in thread_content for name, _ in agents): + #print("Thread Content:", thread_content) + print( + f"Skipping response from {agent_name} to prevent agent-to-agent loop." + ) + continue + human_threads.add(from_) + + if message_id not in self.conversation_threads: + self.conversation_threads[message_id] = [thread_content] + + if '' in thread_content: + formatted_email_history = self.format_email_history_html( + thread_content, from_, + datetime.now().strftime('%a, %b %d, %Y at %I:%M %p')) + else: + formatted_email_history = self.format_email_history_plain( + thread_content, from_, + datetime.now().strftime('%a, %b %d, %Y at %I:%M %p')) + + + self.conversation_threads[message_id].append(formatted_email_history) + + previous_responses.append(response) + + agent = self.agent_loader.get_agent(agent_name) + if agent: + to_emails = [ + email for email in to_emails if email.lower() != from_.lower() + and email.lower() != agent["email"].lower() + ] + cc_emails = [ + email for email in cc_emails if email.lower() != from_.lower() + and email.lower() != agent["email"].lower() + ] + + to_emails_without_agent = [ + email for email in to_emails + if email.lower() != self.smtp_username.lower() + ] + cc_emails_without_agent = [ + email for email in cc_emails + if email.lower() != self.smtp_username.lower() + ] + + if from_ not in to_emails_without_agent and from_ != self.smtp_username: + to_emails_without_agent.append(from_) + + if to_emails_without_agent or cc_emails_without_agent: + print(f"Sending email to: {to_emails_without_agent}") + print(f"CC: {cc_emails_without_agent}") + + if message_id in self.conversation_threads: + self.conversation_threads[message_id].append(response) + + # Collect the email history of the thread + email_history = '\n'.join( + self.conversation_threads.get(message_id, [])[:-1]) + + # Format the email history based on content type + formatted_email_history_html = self.format_email_history_html( + email_history, from_, + datetime.now().strftime('%a, %b %d, %Y at %I:%M %p')) + + formatted_email_history_plain = self.format_email_history_plain( + email_history, from_, + datetime.now().strftime('%a, %b %d, %Y at %I:%M %p')) + + # Formatting response and history in both plain text and HTML + response_plain = MIMEText(response, 'plain', 'utf-8') + response_plain.add_header('Content-Transfer-Encoding', + 'quoted-printable') + + response_with_breaks = response.replace('\n', '
') + + response_html = MIMEText( + f"{response_with_breaks}", 'html', + 'utf-8') + response_html.add_header('Content-Transfer-Encoding', + 'quoted-printable') + + history_plain = MIMEText(formatted_email_history_plain, 'plain') + history_html = MIMEText( + f"{formatted_email_history_html}", + 'html') + + + + alternative_response = MIMEMultipart('alternative') + #alternative_response.attach(response_plain) + + alternative_response.attach(response_html) + + + alternative_history = MIMEMultipart('alternative') + alternative_history.attach(history_plain) + + #alternative_history.attach(history_html) + + + # Creating 'mixed' MIME container for the entire email + msg = MIMEMultipart('mixed') + msg.attach(alternative_response) + + msg.attach(alternative_history) + + + + + try: + self.send_email(from_email=self.smtp_username, + from_alias=agent["email"], + to_emails=to_emails_without_agent, + cc_emails=cc_emails_without_agent, + subject=f"Re: {subject}", + msg=msg, + message_id=message_id, + references=references) + print("Email sent successfully.") + except Exception as e: + print(f"Exception while handling incoming email: {e}") + import traceback + print(traceback.format_exc()) + logging.error(f"Exception while handling incoming email: {e}") + return False + else: + print(".") + + if all_responses_successful: + # Concatenate all previous responses into a single string + full_response_content = "\n\n".join(previous_responses) + + # Format the entire conversation history with 'gmail_quote' div only once + formatted_email_history_html = self.format_email_history_html( + full_response_content, from_, + datetime.now().strftime('%a, %b %d, %Y at %I:%M %p')) + + if not references: + print("No references found, possibly the first email in the thread.") + references = message_id + + x_gm_thrid = references.split()[0] + + self.update_processed_threads(message_id, x_gm_thrid, num, subject, + in_reply_to, references, from_, + ','.join(to_emails + cc_emails)) + + if message_id in self.conversation_threads: + conversation_history = '\n'.join(self.conversation_threads[message_id]) + # print(f"START Conversation history: {conversation_history} END CONVERSATION HISTORY") + + return all_responses_successful + + except Exception as e: + print(f"Exception in handle_incoming_email: {e}") + import traceback + print(traceback.format_exc()) + return False + + # FORMAT EMAIL HISTORY + + def format_email_history_html(self, history, from_email, date): + + try: + decoded_history = quopri.decodestring(history.encode()).decode('utf-8') + except UnicodeDecodeError: + # If utf-8 decoding fails, fall back to 'latin1' encoding + decoded_history = quopri.decodestring(history.encode()).decode('latin1') + + # Detect if history is already wrapped in 'gmail_quote' and avoid re-wrapping + if '
' not in decoded_history: + lines = decoded_history.split('\n') + formatted_lines = ['
{}
'.format(line) for line in lines if line.strip()] + combined_history = '\n'.join(formatted_lines) + html_content = '
On {} {} wrote:
{}
'.format(date, from_email, combined_history) + else: + html_content = decoded_history # Use as-is if already wrapped + + return html_content + + + + def format_email_history_plain(self, history, from_email, date): + # Attempt to decode the history with utf-8, replace characters that are not utf-8 + try: + decoded_history = quopri.decodestring(history.encode()).decode('utf-8') + except UnicodeDecodeError: + # If utf-8 decoding fails, fall back to 'latin1' encoding + decoded_history = quopri.decodestring(history.encode()).decode('latin1') + + lines = decoded_history.split('\n') + quoted_lines = ['> {}'.format(line) for line in lines if line.strip()] + plain_text_content = 'On {} {} wrote:\n{}\n'.format(date, from_email, '\n'.join(quoted_lines)) + return plain_text_content + + def extract_email_content(self, email_message): + """ + Extracts content and attachments from an email message. + + Parameters: + email_message (email.message.Message): The email message object. + + Returns: + str: The combined text content of the email. + list: A list of dictionaries for each PDF attachment found. + """ + content = "" + pdf_attachments = [] + for part in email_message.walk(): + content_type = part.get_content_type() + content_disposition = part.get("Content-Disposition", "") + if content_type == 'text/plain' and 'attachment' not in content_disposition: + payload = part.get_payload(decode=True) + charset = part.get_content_charset('utf-8') + content += payload.decode(charset, errors="replace") + elif content_type == 'application/pdf': + pdf_data = part.get_payload(decode=True) + pdf_text = extract_pdf_text(pdf_data) # Assuming this function is defined elsewhere + pdf_attachments.append({'filename': part.get_filename(), 'text': pdf_text}) + + return content, pdf_attachments + + # SEND EMAIL + + def send_email(self, + from_email, + from_alias, + to_emails, + cc_emails, + subject, + msg, + message_id=None, + references=None, + x_gm_thrid=None): + all_recipients = to_emails + cc_emails + + # Remove the sending agent's email from the To and Cc fields + all_recipients = [ + email for email in all_recipients + if email.lower() != from_email.lower() + ] + + is_html_email = any(part.get_content_type() == 'text/html' + for part in msg.get_payload()) + msg['Content-Type'] = 'text/html; charset="utf-8"' if is_html_email else 'text/plain; charset="utf-8"' + + if not all_recipients: + print("No valid recipients found. Will abort email send.") + return + + msg['From'] = f'"{from_alias}" <{from_alias}>' + msg['Reply-To'] = f'"{from_alias}" <{from_alias}>' + msg['Sender'] = f'"{from_alias}" <{from_alias}>' + msg['To'] = ', '.join(to_emails) + if cc_emails: + msg['Cc'] = ', '.join(cc_emails) + + msg['Subject'] = subject + + if x_gm_thrid: + msg["X-GM-THRID"] = x_gm_thrid + + if message_id: + msg["In-Reply-To"] = message_id + if references: + msg["References"] = references + ' ' + message_id + + # Clean the all_recipients list to remove the SMTP username + all_recipients = [ + email for email in all_recipients + if email.lower() != self.smtp_username.lower() + ] + + + """ + LOGGING + + # Generate a unique filename for the log file + log_filename = f'logs/email_log_{datetime.now().strftime("%Y%m%d%H%M%S")}_{message_id}.txt' + + + # Log the email content + with open(log_filename, 'w') as log_file: + log_file.write(f"From: {from_alias} <{from_alias}>\n") + log_file.write(f"To: {', '.join(to_emails)}\n") + if cc_emails: + log_file.write(f"Cc: {', '.join(cc_emails)}\n") + log_file.write(f"Subject: {subject}\n") + log_file.write("Message Body:\n") + log_file.write(msg.as_string()) + + """ + with self.smtp_connection() as server: + server.sendmail(from_email, all_recipients, msg.as_string()) + print( + f"Email sent with Thread ID: {x_gm_thrid}, Subject: {subject}, Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) diff --git a/email_client.log b/email_client.log new file mode 100755 index 00000000..69847632 --- /dev/null +++ b/email_client.log @@ -0,0 +1,242 @@ +2024-02-23 20:57:39,656 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:39] "GET / HTTP/1.1" 200 - +2024-02-23 20:57:39,799 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:39] "GET /static/main.css HTTP/1.1" 200 - +2024-02-23 20:57:39,831 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:39] "GET /agents/pics/6b251dd9-b336-4ce4-8002-0c7e079f2bc6.png HTTP/1.1" 200 - +2024-02-23 20:57:39,831 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:39] "GET /agents/pics/9ce541fa-6ccd-4938-93ad-e29405b520fc.png HTTP/1.1" 200 - +2024-02-23 20:57:39,957 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:39] "GET /agents/pics/0bdb90f6-d2c8-408b-abdd-cb724d5ba6ba.png HTTP/1.1" 200 - +2024-02-23 20:57:39,987 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:39] "GET /agents/pics/a6b2d3bf-0daf-4383-8984-ad990429c374.png HTTP/1.1" 200 - +2024-02-23 20:57:39,989 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:39] "GET /agents/pics/977e7696-f898-4da3-b295-88936db01028.png HTTP/1.1" 200 - +2024-02-23 20:57:39,989 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:39] "GET /static/content.json HTTP/1.1" 200 - +2024-02-23 20:57:40,030 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:40] "GET /agents/pics/5a8b5285-68ff-419f-84dc-440774a58cf8.png HTTP/1.1" 200 - +2024-02-23 20:57:40,112 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:40] "GET /agents/pics/243455ab-eb20-427a-9343-0a813cb3d759.png HTTP/1.1" 200 - +2024-02-23 20:57:40,409 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:40] "GET /agents/pics/19a4d6db-9148-499e-848a-9c4bcd9b6b38.png HTTP/1.1" 200 - +2024-02-23 20:57:40,867 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:40] "GET /agents/pics/451da327-6b04-4f98-8208-4f249ef11aaf.png HTTP/1.1" 200 - +2024-02-23 20:57:40,977 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:40] "GET /agents/pics/cover_photo.png HTTP/1.1" 200 - +2024-02-23 20:57:41,076 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:41] "GET /agents/pics/81770a8a-78fc-40da-a21d-8f49eccf7f35.png HTTP/1.1" 200 - +2024-02-23 20:57:41,173 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:41] "GET /agents/pics/7b32abaa-e75b-4f22-bc67-082db025dece.png HTTP/1.1" 200 - +2024-02-23 20:57:41,421 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:41] "GET /agents/pics/f9d75dff-f3ae-49c7-9d15-9f21acc5f9f4.png HTTP/1.1" 200 - +2024-02-23 20:57:41,633 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:41] "GET /agents/pics/1acb636e-2ce8-4d70-8e1d-18609e8626bb.png HTTP/1.1" 200 - +2024-02-23 20:57:41,747 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:41] "GET /agents/pics/3ce264f2-8d98-4b10-b65d-0f01ce8fc43e.png HTTP/1.1" 200 - +2024-02-23 20:57:41,832 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:41] "GET /agents/pics/edd4c5d5-d993-4735-984c-f6ee4ad2f96b.png HTTP/1.1" 200 - +2024-02-23 20:57:42,243 [INFO]: 192.168.3.122 - - [23/Feb/2024 20:57:42] "GET /static/favicon.ico HTTP/1.1" 200 - +2024-02-23 21:18:14,065 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-23 21:18:14,065 [INFO]: Press CTRL+C to quit +2024-02-23 21:20:23,232 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET / HTTP/1.1" 200 - +2024-02-23 21:20:23,283 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/6b251dd9-b336-4ce4-8002-0c7e079f2bc6.png HTTP/1.1" 304 - +2024-02-23 21:20:23,285 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /static/main.css HTTP/1.1" 304 - +2024-02-23 21:20:23,287 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/9ce541fa-6ccd-4938-93ad-e29405b520fc.png HTTP/1.1" 304 - +2024-02-23 21:20:23,294 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/0bdb90f6-d2c8-408b-abdd-cb724d5ba6ba.png HTTP/1.1" 304 - +2024-02-23 21:20:23,297 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/a6b2d3bf-0daf-4383-8984-ad990429c374.png HTTP/1.1" 304 - +2024-02-23 21:20:23,311 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/19a4d6db-9148-499e-848a-9c4bcd9b6b38.png HTTP/1.1" 304 - +2024-02-23 21:20:23,313 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/243455ab-eb20-427a-9343-0a813cb3d759.png HTTP/1.1" 304 - +2024-02-23 21:20:23,314 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/5a8b5285-68ff-419f-84dc-440774a58cf8.png HTTP/1.1" 304 - +2024-02-23 21:20:23,316 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/977e7696-f898-4da3-b295-88936db01028.png HTTP/1.1" 304 - +2024-02-23 21:20:23,321 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/451da327-6b04-4f98-8208-4f249ef11aaf.png HTTP/1.1" 304 - +2024-02-23 21:20:23,322 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/81770a8a-78fc-40da-a21d-8f49eccf7f35.png HTTP/1.1" 304 - +2024-02-23 21:20:23,326 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/7b32abaa-e75b-4f22-bc67-082db025dece.png HTTP/1.1" 304 - +2024-02-23 21:20:23,332 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/1acb636e-2ce8-4d70-8e1d-18609e8626bb.png HTTP/1.1" 304 - +2024-02-23 21:20:23,333 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/f9d75dff-f3ae-49c7-9d15-9f21acc5f9f4.png HTTP/1.1" 304 - +2024-02-23 21:20:23,335 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/3ce264f2-8d98-4b10-b65d-0f01ce8fc43e.png HTTP/1.1" 304 - +2024-02-23 21:20:23,338 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/edd4c5d5-d993-4735-984c-f6ee4ad2f96b.png HTTP/1.1" 304 - +2024-02-23 21:20:23,351 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /static/content.json HTTP/1.1" 304 - +2024-02-23 21:20:23,461 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /static/favicon.ico HTTP/1.1" 304 - +2024-02-23 21:20:23,462 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:20:23] "GET /agents/pics/cover_photo.png HTTP/1.1" 304 - +2024-02-23 21:21:05,926 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:05] "GET / HTTP/1.1" 200 - +2024-02-23 21:21:06,087 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:06] "GET /static/main.css HTTP/1.1" 200 - +2024-02-23 21:21:06,088 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:06] "GET /agents/pics/9ce541fa-6ccd-4938-93ad-e29405b520fc.png HTTP/1.1" 200 - +2024-02-23 21:21:06,089 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:06] "GET /agents/pics/6b251dd9-b336-4ce4-8002-0c7e079f2bc6.png HTTP/1.1" 200 - +2024-02-23 21:21:06,238 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:06] "GET /agents/pics/0bdb90f6-d2c8-408b-abdd-cb724d5ba6ba.png HTTP/1.1" 200 - +2024-02-23 21:21:06,317 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:06] "GET /static/content.json HTTP/1.1" 200 - +2024-02-23 21:21:06,320 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:06] "GET /agents/pics/a6b2d3bf-0daf-4383-8984-ad990429c374.png HTTP/1.1" 200 - +2024-02-23 21:21:06,321 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:06] "GET /agents/pics/977e7696-f898-4da3-b295-88936db01028.png HTTP/1.1" 200 - +2024-02-23 21:21:06,336 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:06] "GET /agents/pics/5a8b5285-68ff-419f-84dc-440774a58cf8.png HTTP/1.1" 200 - +2024-02-23 21:21:06,362 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:06] "GET /agents/pics/243455ab-eb20-427a-9343-0a813cb3d759.png HTTP/1.1" 200 - +2024-02-23 21:21:06,369 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:06] "GET /agents/pics/19a4d6db-9148-499e-848a-9c4bcd9b6b38.png HTTP/1.1" 200 - +2024-02-23 21:21:07,048 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:07] "GET /agents/pics/451da327-6b04-4f98-8208-4f249ef11aaf.png HTTP/1.1" 200 - +2024-02-23 21:21:07,070 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:07] "GET /agents/pics/81770a8a-78fc-40da-a21d-8f49eccf7f35.png HTTP/1.1" 200 - +2024-02-23 21:21:07,153 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:07] "GET /agents/pics/7b32abaa-e75b-4f22-bc67-082db025dece.png HTTP/1.1" 200 - +2024-02-23 21:21:07,450 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:07] "GET /agents/pics/f9d75dff-f3ae-49c7-9d15-9f21acc5f9f4.png HTTP/1.1" 200 - +2024-02-23 21:21:07,574 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:07] "GET /agents/pics/1acb636e-2ce8-4d70-8e1d-18609e8626bb.png HTTP/1.1" 200 - +2024-02-23 21:21:07,901 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:07] "GET /agents/pics/3ce264f2-8d98-4b10-b65d-0f01ce8fc43e.png HTTP/1.1" 200 - +2024-02-23 21:21:08,060 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:08] "GET /agents/pics/edd4c5d5-d993-4735-984c-f6ee4ad2f96b.png HTTP/1.1" 200 - +2024-02-23 21:21:08,310 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:08] "GET /agents/pics/cover_photo.png HTTP/1.1" 200 - +2024-02-23 21:21:09,123 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:21:09] "GET /static/favicon.ico HTTP/1.1" 200 - +2024-02-23 21:38:29,859 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-23 21:38:29,859 [INFO]: Press CTRL+C to quit +2024-02-23 21:44:10,021 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET / HTTP/1.1" 200 - +2024-02-23 21:44:10,063 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /static/main.css HTTP/1.1" 304 - +2024-02-23 21:44:10,087 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/9ce541fa-6ccd-4938-93ad-e29405b520fc.png HTTP/1.1" 304 - +2024-02-23 21:44:10,088 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/6b251dd9-b336-4ce4-8002-0c7e079f2bc6.png HTTP/1.1" 304 - +2024-02-23 21:44:10,114 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/a6b2d3bf-0daf-4383-8984-ad990429c374.png HTTP/1.1" 304 - +2024-02-23 21:44:10,114 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/0bdb90f6-d2c8-408b-abdd-cb724d5ba6ba.png HTTP/1.1" 304 - +2024-02-23 21:44:10,115 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/5a8b5285-68ff-419f-84dc-440774a58cf8.png HTTP/1.1" 304 - +2024-02-23 21:44:10,117 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/977e7696-f898-4da3-b295-88936db01028.png HTTP/1.1" 304 - +2024-02-23 21:44:10,127 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/243455ab-eb20-427a-9343-0a813cb3d759.png HTTP/1.1" 304 - +2024-02-23 21:44:10,128 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/19a4d6db-9148-499e-848a-9c4bcd9b6b38.png HTTP/1.1" 304 - +2024-02-23 21:44:10,215 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/451da327-6b04-4f98-8208-4f249ef11aaf.png HTTP/1.1" 304 - +2024-02-23 21:44:10,217 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/81770a8a-78fc-40da-a21d-8f49eccf7f35.png HTTP/1.1" 304 - +2024-02-23 21:44:10,219 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/7b32abaa-e75b-4f22-bc67-082db025dece.png HTTP/1.1" 304 - +2024-02-23 21:44:10,222 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/f9d75dff-f3ae-49c7-9d15-9f21acc5f9f4.png HTTP/1.1" 304 - +2024-02-23 21:44:10,228 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/1acb636e-2ce8-4d70-8e1d-18609e8626bb.png HTTP/1.1" 304 - +2024-02-23 21:44:10,235 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/3ce264f2-8d98-4b10-b65d-0f01ce8fc43e.png HTTP/1.1" 304 - +2024-02-23 21:44:10,278 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/edd4c5d5-d993-4735-984c-f6ee4ad2f96b.png HTTP/1.1" 304 - +2024-02-23 21:44:10,626 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /static/content.json HTTP/1.1" 304 - +2024-02-23 21:44:10,647 [INFO]: 192.168.3.122 - - [23/Feb/2024 21:44:10] "GET /agents/pics/cover_photo.png HTTP/1.1" 304 - +2024-02-23 23:33:02,897 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET / HTTP/1.1" 200 - +2024-02-23 23:33:02,928 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /static/main.css HTTP/1.1" 304 - +2024-02-23 23:33:02,929 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/9ce541fa-6ccd-4938-93ad-e29405b520fc.png HTTP/1.1" 304 - +2024-02-23 23:33:02,931 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/6b251dd9-b336-4ce4-8002-0c7e079f2bc6.png HTTP/1.1" 304 - +2024-02-23 23:33:02,946 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/a6b2d3bf-0daf-4383-8984-ad990429c374.png HTTP/1.1" 304 - +2024-02-23 23:33:02,947 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/0bdb90f6-d2c8-408b-abdd-cb724d5ba6ba.png HTTP/1.1" 304 - +2024-02-23 23:33:02,963 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/243455ab-eb20-427a-9343-0a813cb3d759.png HTTP/1.1" 304 - +2024-02-23 23:33:02,966 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/977e7696-f898-4da3-b295-88936db01028.png HTTP/1.1" 304 - +2024-02-23 23:33:02,967 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/5a8b5285-68ff-419f-84dc-440774a58cf8.png HTTP/1.1" 304 - +2024-02-23 23:33:02,969 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/19a4d6db-9148-499e-848a-9c4bcd9b6b38.png HTTP/1.1" 304 - +2024-02-23 23:33:02,974 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /static/content.json HTTP/1.1" 304 - +2024-02-23 23:33:02,984 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/451da327-6b04-4f98-8208-4f249ef11aaf.png HTTP/1.1" 304 - +2024-02-23 23:33:02,990 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/81770a8a-78fc-40da-a21d-8f49eccf7f35.png HTTP/1.1" 304 - +2024-02-23 23:33:02,993 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/7b32abaa-e75b-4f22-bc67-082db025dece.png HTTP/1.1" 304 - +2024-02-23 23:33:02,994 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/f9d75dff-f3ae-49c7-9d15-9f21acc5f9f4.png HTTP/1.1" 304 - +2024-02-23 23:33:02,997 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:02] "GET /agents/pics/3ce264f2-8d98-4b10-b65d-0f01ce8fc43e.png HTTP/1.1" 304 - +2024-02-23 23:33:03,001 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:03] "GET /agents/pics/edd4c5d5-d993-4735-984c-f6ee4ad2f96b.png HTTP/1.1" 304 - +2024-02-23 23:33:03,002 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:03] "GET /agents/pics/1acb636e-2ce8-4d70-8e1d-18609e8626bb.png HTTP/1.1" 304 - +2024-02-23 23:33:03,060 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:03] "GET /static/favicon.ico HTTP/1.1" 304 - +2024-02-23 23:33:03,062 [INFO]: 192.168.3.122 - - [23/Feb/2024 23:33:03] "GET /agents/pics/cover_photo.png HTTP/1.1" 304 - +2024-02-24 06:15:09,054 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 06:15:09,054 [INFO]: Press CTRL+C to quit +2024-02-24 08:06:16,855 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:16] "GET / HTTP/1.1" 200 - +2024-02-24 08:06:16,969 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:16] "GET /agents/pics/6b251dd9-b336-4ce4-8002-0c7e079f2bc6.png HTTP/1.1" 304 - +2024-02-24 08:06:16,970 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:16] "GET /agents/pics/9ce541fa-6ccd-4938-93ad-e29405b520fc.png HTTP/1.1" 304 - +2024-02-24 08:06:16,971 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:16] "GET /static/main.css HTTP/1.1" 304 - +2024-02-24 08:06:16,998 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:16] "GET /agents/pics/0bdb90f6-d2c8-408b-abdd-cb724d5ba6ba.png HTTP/1.1" 304 - +2024-02-24 08:06:17,000 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/a6b2d3bf-0daf-4383-8984-ad990429c374.png HTTP/1.1" 304 - +2024-02-24 08:06:17,004 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/977e7696-f898-4da3-b295-88936db01028.png HTTP/1.1" 304 - +2024-02-24 08:06:17,012 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/5a8b5285-68ff-419f-84dc-440774a58cf8.png HTTP/1.1" 304 - +2024-02-24 08:06:17,012 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/243455ab-eb20-427a-9343-0a813cb3d759.png HTTP/1.1" 304 - +2024-02-24 08:06:17,020 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/19a4d6db-9148-499e-848a-9c4bcd9b6b38.png HTTP/1.1" 304 - +2024-02-24 08:06:17,022 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/7b32abaa-e75b-4f22-bc67-082db025dece.png HTTP/1.1" 304 - +2024-02-24 08:06:17,023 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/451da327-6b04-4f98-8208-4f249ef11aaf.png HTTP/1.1" 304 - +2024-02-24 08:06:17,023 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/81770a8a-78fc-40da-a21d-8f49eccf7f35.png HTTP/1.1" 304 - +2024-02-24 08:06:17,028 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/1acb636e-2ce8-4d70-8e1d-18609e8626bb.png HTTP/1.1" 304 - +2024-02-24 08:06:17,028 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/f9d75dff-f3ae-49c7-9d15-9f21acc5f9f4.png HTTP/1.1" 304 - +2024-02-24 08:06:17,032 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/3ce264f2-8d98-4b10-b65d-0f01ce8fc43e.png HTTP/1.1" 304 - +2024-02-24 08:06:17,037 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/edd4c5d5-d993-4735-984c-f6ee4ad2f96b.png HTTP/1.1" 304 - +2024-02-24 08:06:17,080 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /static/content.json HTTP/1.1" 304 - +2024-02-24 08:06:17,236 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /agents/pics/cover_photo.png HTTP/1.1" 304 - +2024-02-24 08:06:17,237 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:06:17] "GET /static/favicon.ico HTTP/1.1" 304 - +2024-02-24 08:07:16,669 [INFO]: 192.168.3.122 - - [24/Feb/2024 08:07:16] "GET /favicon.ico HTTP/1.1" 404 - +2024-02-24 09:08:16,821 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 09:08:16,822 [INFO]: Press CTRL+C to quit +2024-02-24 09:51:44,939 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 09:51:44,939 [INFO]: Press CTRL+C to quit +2024-02-24 09:57:32,340 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 09:57:32,340 [INFO]: Press CTRL+C to quit +2024-02-24 10:09:53,321 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 10:09:53,321 [INFO]: Press CTRL+C to quit +2024-02-24 10:11:37,643 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 10:11:37,643 [INFO]: Press CTRL+C to quit +2024-02-24 10:11:43,711 [WARNING]: No valid agent info found +2024-02-24 10:12:07,604 [WARNING]: No valid agent info found +2024-02-24 10:13:01,893 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 10:13:01,893 [INFO]: Press CTRL+C to quit +2024-02-24 10:13:07,977 [WARNING]: No valid agent info found +2024-02-24 10:15:49,614 [WARNING]: No valid agent info found +2024-02-24 10:20:04,228 [WARNING]: No valid agent info found +2024-02-24 10:22:52,308 [WARNING]: No valid agent info found +2024-02-24 10:22:52,310 [WARNING]: No valid agent info found +2024-02-24 10:26:39,794 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 10:26:39,795 [INFO]: Press CTRL+C to quit +2024-02-24 10:26:45,868 [WARNING]: No valid agent info found +2024-02-24 10:30:33,747 [WARNING]: No valid agent info found +2024-02-24 10:31:58,107 [WARNING]: No valid agent info found +2024-02-24 10:33:55,554 [ERROR]: Exception while handling incoming email: EmailClient.send_email() got an unexpected keyword argument 'msg' +2024-02-24 10:35:15,042 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 10:35:15,042 [INFO]: Press CTRL+C to quit +2024-02-24 10:36:08,004 [ERROR]: Exception while handling incoming email: EmailClient.send_email() got an unexpected keyword argument 'msg' +2024-02-24 10:40:40,231 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 10:40:40,231 [INFO]: Press CTRL+C to quit +2024-02-24 11:01:23,446 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 11:01:23,447 [INFO]: Press CTRL+C to quit +2024-02-24 11:17:02,325 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 11:17:02,325 [INFO]: Press CTRL+C to quit +2024-02-24 11:17:08,404 [WARNING]: No valid agent info found +2024-02-24 11:17:54,885 [ERROR]: Exception while handling incoming email: EmailClient.send_email() got an unexpected keyword argument 'msg' +2024-02-24 11:18:00,890 [WARNING]: No valid agent info found +2024-02-24 11:19:42,010 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 11:19:42,010 [INFO]: Press CTRL+C to quit +2024-02-24 11:20:40,897 [ERROR]: Exception while handling incoming email: EmailClient.send_email() got an unexpected keyword argument 'msg' +2024-02-24 11:20:46,905 [WARNING]: No valid agent info found +2024-02-24 11:35:17,001 [WARNING]: No valid agent info found +2024-02-24 12:41:06,111 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 12:41:06,112 [INFO]: Press CTRL+C to quit +2024-02-24 13:11:39,681 [ERROR]: Exception while handling incoming email: EmailClient.clean_html_content() takes 1 positional argument but 2 were given +2024-02-24 13:24:17,576 [ERROR]: Exception while handling incoming email: EmailClient.send_email() got multiple values for argument 'from_email' +2024-02-24 13:32:09,201 [ERROR]: Exception while handling incoming email: EmailClient.send_email() got multiple values for argument 'from_email' +2024-02-24 13:35:18,352 [ERROR]: Exception while handling incoming email: EmailClient.send_email() got multiple values for argument 'from_email' +2024-02-24 13:41:26,853 [ERROR]: Exception while handling incoming email: EmailClient.send_email() got multiple values for argument 'from_email' +2024-02-24 13:42:51,193 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 13:42:51,193 [INFO]: Press CTRL+C to quit +2024-02-24 13:47:51,037 [ERROR]: Exception while handling incoming email: EmailClient.send_email() got multiple values for argument 'from_alias' +2024-02-24 13:50:33,501 [ERROR]: Exception while handling incoming email: name 'escaped_recipients' is not defined +2024-02-24 13:54:29,776 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 13:54:29,777 [INFO]: Press CTRL+C to quit +2024-02-24 14:34:03,426 [INFO]: WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:81 + * Running on http://192.168.3.18:81 +2024-02-24 14:34:03,426 [INFO]: Press CTRL+C to quit +2024-02-24 14:35:21,392 [WARNING]: No valid agent info found +2024-02-24 14:49:28,169 [WARNING]: No valid agent info found diff --git a/email_client.py b/email_client.py old mode 100644 new mode 100755 index dfdaf624..7492b323 --- a/email_client.py +++ b/email_client.py @@ -1,4 +1,5 @@ import email +from bs4 import BeautifulSoup import html import imaplib import json @@ -8,8 +9,9 @@ import smtplib import quopri import tempfile +import subprocess import shutil - +import requests from time import sleep from datetime import datetime from email.mime.multipart import MIMEMultipart @@ -17,14 +19,20 @@ from email.utils import getaddresses from contextlib import contextmanager +import subprocess +import shlex + from pdf2text import extract_pdf_text from shortcode import handle_document_short_code from agent_operator import AgentSelector +from dotenv import load_dotenv +import os -class EmailClient: +load_dotenv() - # INIT & SETUP EMAIL ACCESS + +class EmailClient: def __init__(self, agent_loader, gpt, testing=False): logging.basicConfig(filename='email_client.log', @@ -39,10 +47,31 @@ def __init__(self, agent_loader, gpt, testing=False): self.conversation_threads = {} self.openai_api_key = os.getenv('OPENAI_API_KEY') + def get_response_from_api(self, content): + print("api") + + """Get response from external API for the given content.""" + url = "http://127.0.0.1:5000/v1/chat/completions" + headers = {"Content-Type": "application/json"} + data = { + "mode": "chat", + "character": "Example", + "messages": [{"role": "user", "content": content}] + } + response = requests.post(url, headers=headers, json=data, verify=False) + if response.status_code == 200: + api_response = response.json() + assistant_message = api_response['choices'][0]['message']['content'] + return assistant_message + else: + print("Failed to get response from API") + return None + def setup_email_client(self): self.smtp_server = os.getenv('SMTP_SERVER') self.smtp_port = os.getenv('SMTP_PORT') self.smtp_username = os.getenv('SMTP_USERNAME') + print('self.smtp_username',self.smtp_username) self.smtp_password = os.getenv('SMTP_PASSWORD') self.connect_to_imap_server() @@ -59,11 +88,56 @@ def check_imap_connection(self): except: return False + def extract_content_from_multipart(self, msg): + """ + Extracts and returns the plain text or HTML content from a MIMEMultipart email message. + + Parameters: + msg (MIMEMultipart): The multipart email message object. + + Returns: + str: The extracted content as a plain string. + """ + text_content = "" + for part in msg.walk(): + # Check if the part is plain text or HTML + if part.get_content_type() == "text/plain" or part.get_content_type() == "text/html": + text_content += part.get_payload(decode=True).decode() + # Optionally, break after finding the first text/plain or text/html part + break + return text_content + + def sanitize_email_address(self, email_address): + # Regular expression for validating an Email + regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' + if re.fullmatch(regex, email_address): + return email_address + else: + print("Invalid Email Address. Making adjustments.") + # Perform any necessary adjustments or fallback + # For demonstration, returning a default email if validation fails + return "default@localdomain.com" + def start(self): print("Started email server") restart_counter = 0 MAX_RESTARTS = 500 - sleep_time = 620 # Sleep time in seconds + sleep_time = 6 # Sleep time in seconds + + # Directly prepare and send a test email + if(False): + try: + self.send_email( + from_email="rocket@localdomain.com", # Use an actual sender email address valid on your SMTP server + from_alias="Test Sender", # Sender's name or alias + to_emails=["root@localdomain.com"], # Replace with actual recipient email address + cc_emails=[], # CC emails if any, otherwise leave empty + subject="Test Email from start method", + modified_content="This is a test email sent from the start method." # Email body content + ) + print("Test email sent successfully.") + except Exception as e: + print(f"Failed to send test email: {e}") while True: try: @@ -96,9 +170,9 @@ def run_server_loop(self, sleep_time): for i in range( sleep_time, 0, - -30): # Decreasing sleep time by 10 seconds in each iteration + -3): # Decreasing sleep time by 10 seconds in each iteration print(f"Sleeping... {i} seconds remaining.") - sleep(30) # Sleep for 10 seconds + sleep(3) # Sleep for 10 seconds timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") print(f"[{timestamp}] Resuming processing.") @@ -203,100 +277,63 @@ def is_email_processed(self, x_gm_thrid, num): return False def load_email(self, num): + """ + Load and parse an email by its unique ID. + + Parameters: + num (str): The unique ID of the email to load. + + Returns: + tuple: A tuple containing parsed email components or None values on failure. + """ try: # Standardize the num to string type num_str = num.decode('utf-8') if isinstance(num, bytes) else str(num) - + + # Initialize the return values + from_, to_emails, cc_emails, subject, message_id, in_reply_to, references, content = (None,) * 8 + # Fetch the email content by UID - result, data = self.imap_server.uid('fetch', num_str, - '(RFC822 X-GM-THRID)') + result, data = self.imap_server.uid('fetch', num_str, '(RFC822)') if result != 'OK': print(f"Error fetching email content for UID {num}: {result}") return None, None, None, None, None, None, None, None - - # Initialize variables - x_gm_thrid = None + raw_email = None for response_part in data: if isinstance(response_part, tuple): - match = re.search(r'X-GM-THRID (\d+)', - response_part[0].decode('utf-8')) - if match: - x_gm_thrid = match.group(1) raw_email = response_part[1].decode("utf-8") - - email_message = email.message_from_string(raw_email) - if not email_message: - print(f"Error: email_message object is None for UID {num}") + break + + if not raw_email: + print(f"No email content found for UID {num}") return None, None, None, None, None, None, None, None - - # Extract headers and content + + email_message = email.message_from_string(raw_email) + + # Extract headers from_ = email_message['From'] to_emails = [addr[1] for addr in getaddresses([email_message['To']])] - cc_emails = [ - addr[1] for addr in getaddresses([email_message.get('Cc', '')]) - ] + cc_emails = [addr[1] for addr in getaddresses([email_message.get('Cc', '')])] subject = email_message['Subject'] message_id = email_message['Message-ID'] in_reply_to = email_message.get('In-Reply-To', '') references = email_message.get('References', '') - - content = "" - pdf_attachments = [] - - # Extract body and PDF content - for part in email_message.walk(): - if part.get_content_type() == 'text/html': - part.get_payload(decode=True).decode('utf-8') - elif part.get_content_type() == 'text/plain': - part.get_payload(decode=True).decode('utf-8') - content_encoding = part.get("Content-Transfer-Encoding") - payload = part.get_payload() - if content_encoding == 'base64': - content += base64.b64decode(payload).decode('utf-8') - else: - content += payload - - elif part.get_content_type() == 'application/pdf': - pdf_data = part.get_payload(decode=True) - pdf_text = extract_pdf_text(pdf_data) - pdf_attachments.append({ - 'filename': part.get_filename(), - 'text': pdf_text - }) - - # Append PDF contents to the content + + # Extract body and attachments + content, pdf_attachments = self.extract_email_content(email_message) + print("load_email") + print(content) + # Append PDF contents to the email content for pdf_attachment in pdf_attachments: - pdf_text = pdf_attachment.get('text', '') - if pdf_text: - pdf_label = f"PDF: {pdf_attachment['filename']}" - content += f"\n\n{pdf_label}\n{pdf_text}\n{pdf_label}\n" - - MAX_LIMIT = 350000 - if "!detail" in content or re.search(r"!summarize\.", content): - MAX_LIMIT = 2000000 - - # Check if the content is too long - if len(content) > MAX_LIMIT: - print(f"Content too long for UID {num}: {len(content)} characters.") - self.send_error_email(from_, subject, "Content too long") - self.mark_as_seen(num) - self.update_processed_threads(message_id, x_gm_thrid, num, subject, - in_reply_to, references, from_, - ','.join(to_emails + cc_emails)) - return None, None, None, None, None, None, None, None - - # Return the content which now includes both the email body and the PDF contents - return message_id, num, subject, content, from_, to_emails, cc_emails, references, in_reply_to, x_gm_thrid - - except Exception as e: - print(f"Exception while processing email with UID {num}: {e}") - import traceback - print(traceback.format_exc()) - return None, None, None, None, None, None, None, None + pdf_label = f"PDF: {pdf_attachment['filename']}" + content += f"\n\n{pdf_label}\n{pdf_attachment['text']}\n{pdf_label}\n" + except: + print("load email except") + + return message_id, num, subject, content, from_, to_emails, cc_emails, references, in_reply_to def process_thread(self): - try: self.imap_server.select("INBOX") result, data = self.imap_server.uid('search', None, 'UNSEEN') if result != 'OK': @@ -306,32 +343,29 @@ def process_thread(self): unseen_emails = data[0].split() thread_to_unseen = {} - # Group unseen emails by thread + # Group unseen emails by subject and sender as a proxy for thread grouping for num in unseen_emails: + print(num) email_data = self.load_email(num) - if email_data is None or len(email_data) != 10: - continue - message_id, _, subject, _, from_, _, _, _, _, x_gm_thrid = email_data - if self.is_email_processed(x_gm_thrid, num): + if email_data is None or len(email_data) < 8: # Adjusted for the updated return values from load_email continue - - # Use x_gm_thrid if available, else use subject - if x_gm_thrid is None or x_gm_thrid == "": - raise ValueError("x_gm_thrid is unavailable.") - - thread_key = x_gm_thrid + message_id, num_str, subject, content, from_, to_emails, cc_emails, references, in_reply_to = email_data + print("process_thread") + print(content) + # Use a combination of subject and sender for grouping + thread_key = f"{subject}:{from_}" if thread_key not in thread_to_unseen: thread_to_unseen[thread_key] = [] thread_to_unseen[thread_key].append({ - "message_id": message_id, - "num": num, - "subject": subject, - "from_": from_ + "message_id": message_id, + "num": num, + "subject": subject, + "from_": from_ }) - # Process the most recent unseen email in each thread + # Process the most recent unseen email in each pseudo-thread for thread_key, unseen_list in thread_to_unseen.items(): unseen_list.sort(key=lambda x: int(x['num']), reverse=True) most_recent_unseen = unseen_list[0] @@ -342,28 +376,26 @@ def process_thread(self): if not processed: print(f"Failed to process thread: {thread_key}") else: - # Mark all other unseen emails in the same thread as seen + # Mark all other unseen emails in the same pseudo-thread as seen for unseen_email in unseen_list[1:]: self.mark_as_seen(unseen_email['num']) - if not thread_key: - raise ValueError("thread_key is unavailable.") - x_gm_thrid = thread_key - + # Update processed threads without x_gm_thrid, using message_id instead self.update_processed_threads(unseen_email['message_id'], - x_gm_thrid, unseen_email['num'], - unseen_email['subject'], "", "", - unseen_email['from_'], "") - - except Exception as e: - print(f"Exception while processing emails: {e}") - import traceback - print(traceback.format_exc()) + "", # Empty placeholder for x_gm_thrid + unseen_email['num'], + unseen_email['subject'], "", "", + unseen_email['from_'], "") def process_single_thread(self, num): processed = False try: - message_id, num, subject, content, from_, to_emails, cc_emails, references, in_reply_to, x_gm_thrid = self.load_email( - num) + message_id, num_str, subject, content, from_, to_emails, cc_emails, references, in_reply_to = self.load_email(num) + if not message_id: + return processed # Skip if email couldn't be processed + + print("process_single_thread") + print(content) + if not message_id: return processed # Skip if email couldn't be processed @@ -441,6 +473,7 @@ def handle_incoming_email(self, from_, to_emails, cc_emails, thread_content, #print(f"Debug: Email content at start of handle_incoming_email:{thread_content[:50]}") new_content = thread_content + #print("new content",new_content) # Reset conversation history for a new email thread self.agent_operator.reset_for_new_thread() #print("Before human_threads initialization:", from_, to_emails,cc_emails, subject, message_id, references, num) @@ -531,9 +564,10 @@ def handle_incoming_email(self, from_, to_emails, cc_emails, thread_content, previous_responses = [] # Ensure proper unpacking for the load_email function - - message_id, num, subject, content, from_, to_emails, cc_emails, references, in_reply_to, x_gm_thrid = self.load_email( + message_id, num_str, subject, content, from_, to_emails, cc_emails, references, in_reply_to = self.load_email( num) + print("handle_incoming_email") + print(content) missing_values = [ var_name for var_name, value in locals().items() @@ -630,7 +664,7 @@ def handle_incoming_email(self, from_, to_emails, cc_emails, thread_content, ] to_emails_without_agent = [ - email for email in to_emails + self.sanitize_email_address(email) for email in to_emails if email.lower() != self.smtp_username.lower() ] cc_emails_without_agent = [ @@ -641,9 +675,9 @@ def handle_incoming_email(self, from_, to_emails, cc_emails, thread_content, if from_ not in to_emails_without_agent and from_ != self.smtp_username: to_emails_without_agent.append(from_) - if to_emails_without_agent or cc_emails_without_agent: - print(f"Sending email to: {to_emails_without_agent}") - print(f"CC: {cc_emails_without_agent}") + #if to_emails_without_agent or cc_emails_without_agent: + #print(f"Sending email to: {to_emails_without_agent}") + #print(f"CC: {cc_emails_without_agent}") if message_id in self.conversation_threads: self.conversation_threads[message_id].append(response) @@ -679,8 +713,6 @@ def handle_incoming_email(self, from_, to_emails, cc_emails, thread_content, f"{formatted_email_history_html}", 'html') - - alternative_response = MIMEMultipart('alternative') #alternative_response.attach(response_plain) @@ -692,25 +724,24 @@ def handle_incoming_email(self, from_, to_emails, cc_emails, thread_content, #alternative_history.attach(history_html) - # Creating 'mixed' MIME container for the entire email msg = MIMEMultipart('mixed') msg.attach(alternative_response) msg.attach(alternative_history) - - - + modified_content = self.extract_content_from_multipart(msg) try: - self.send_email(from_email=self.smtp_username, + #print('self.smtp_username',self.smtp_username) + self.send_email(from_email='root@localdomain.com', from_alias=agent["email"], to_emails=to_emails_without_agent, cc_emails=cc_emails_without_agent, subject=f"Re: {subject}", - msg=msg, - message_id=message_id, - references=references) + modified_content=modified_content + #message_id=message_id, + #references=references + ) print("Email sent successfully.") except Exception as e: print(f"Exception while handling incoming email: {e}") @@ -788,80 +819,67 @@ def format_email_history_plain(self, history, from_email, date): plain_text_content = 'On {} {} wrote:\n{}\n'.format(date, from_email, '\n'.join(quoted_lines)) return plain_text_content - - - # SEND EMAIL - - def send_email(self, - from_email, - from_alias, - to_emails, - cc_emails, - subject, - msg, - message_id=None, - references=None, - x_gm_thrid=None): - all_recipients = to_emails + cc_emails - - # Remove the sending agent's email from the To and Cc fields - all_recipients = [ - email for email in all_recipients - if email.lower() != from_email.lower() - ] - - is_html_email = any(part.get_content_type() == 'text/html' - for part in msg.get_payload()) - msg['Content-Type'] = 'text/html; charset="utf-8"' if is_html_email else 'text/plain; charset="utf-8"' - - if not all_recipients: - print("No valid recipients found. Will abort email send.") - return - - msg['From'] = f'"{from_alias}" <{from_alias}>' - msg['Reply-To'] = f'"{from_alias}" <{from_alias}>' - msg['Sender'] = f'"{from_alias}" <{from_alias}>' - msg['To'] = ', '.join(to_emails) - if cc_emails: - msg['Cc'] = ', '.join(cc_emails) - - msg['Subject'] = subject - - if x_gm_thrid: - msg["X-GM-THRID"] = x_gm_thrid - - if message_id: - msg["In-Reply-To"] = message_id - if references: - msg["References"] = references + ' ' + message_id - - # Clean the all_recipients list to remove the SMTP username - all_recipients = [ - email for email in all_recipients - if email.lower() != self.smtp_username.lower() - ] - - + def extract_email_content(self, email_message): """ - LOGGING - - # Generate a unique filename for the log file - log_filename = f'logs/email_log_{datetime.now().strftime("%Y%m%d%H%M%S")}_{message_id}.txt' - - - # Log the email content - with open(log_filename, 'w') as log_file: - log_file.write(f"From: {from_alias} <{from_alias}>\n") - log_file.write(f"To: {', '.join(to_emails)}\n") - if cc_emails: - log_file.write(f"Cc: {', '.join(cc_emails)}\n") - log_file.write(f"Subject: {subject}\n") - log_file.write("Message Body:\n") - log_file.write(msg.as_string()) - + Extracts content and attachments from an email message. + + Parameters: + email_message (email.message.Message): The email message object. + + Returns: + str: The combined text content of the email. + list: A list of dictionaries for each PDF attachment found. """ - with self.smtp_connection() as server: - server.sendmail(from_email, all_recipients, msg.as_string()) - print( - f"Email sent with Thread ID: {x_gm_thrid}, Subject: {subject}, Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + content = "" + pdf_attachments = [] + for part in email_message.walk(): + content_type = part.get_content_type() + content_disposition = part.get("Content-Disposition", "") + if content_type == 'text/plain' and 'attachment' not in content_disposition: + payload = part.get_payload(decode=True) + charset = part.get_content_charset('utf-8') + content += payload.decode(charset, errors="replace") + elif content_type == 'application/pdf': + pdf_data = part.get_payload(decode=True) + pdf_text = extract_pdf_text(pdf_data) # Assuming this function is defined elsewhere + pdf_attachments.append({'filename': part.get_filename(), 'text': pdf_text}) + + return content, pdf_attachments + + def clean_html_content(self, html_content): + """Removes HTML tags and returns plain text.""" + soup = BeautifulSoup(html_content, "html.parser") + return soup.get_text(separator="\n") + + def send_email(self, from_email, from_alias, to_emails, cc_emails, subject, modified_content, message_id=None, references=None, x_gm_thrid=None): + """Sends an email using the 'mail' command, handling special characters.""" + recipients = ','.join(to_emails) # Combine all recipients into a single string + cleaned_content = self.clean_html_content(modified_content) # Clean the HTML content + + + recipients = 'root@localdomain.com' + + # Prepare the email content, including headers + email_content = ( + f"From: {from_alias} <{from_email}>\n" + f"To: {recipients}\n" + f"Subject: {subject}\n\n" + f"{cleaned_content}" ) + + # Use a temporary file to store the email content + with tempfile.NamedTemporaryFile(delete=False, mode='w+') as temp_file: + temp_file.write(email_content) + temp_file_path = temp_file.name + + # Send the email by reading from the temporary file + command = f'cat {temp_file_path} | mail -s "{subject}" {recipients}' + print('command',command) + try: + subprocess.run(command, shell=True, check=True, executable='/bin/bash') + print("Email sent successfully using the 'mail' command.") + except subprocess.CalledProcessError as e: + print(f"Failed to send email using 'mail' command: {e}") + + # Clean up the temporary file + #os.remove(temp_file_path) \ No newline at end of file diff --git a/error-response-email.txt b/error-response-email.txt old mode 100644 new mode 100755 diff --git a/gpt.py b/gpt.py old mode 100644 new mode 100755 index 0cadb299..fa8e9fe3 --- a/gpt.py +++ b/gpt.py @@ -1,6 +1,8 @@ import os import time import openai +openai.api_base = 'http://127.0.0.1:5000/v1' +#https://api.openai.com/v1 import tiktoken import re import pickle diff --git a/instructions.json b/instructions.json old mode 100644 new mode 100755 diff --git a/main.py b/main.py old mode 100644 new mode 100755 index 4263a1cd..f2f06788 --- a/main.py +++ b/main.py @@ -3,6 +3,10 @@ import json import os import subprocess +from dotenv import load_dotenv +import os + +load_dotenv() from email_client import EmailClient from gpt import GPTModel diff --git a/pdf2text.py b/pdf2text.py old mode 100644 new mode 100755 diff --git a/processed_threads.json b/processed_threads.json index 28fecc0e..d2190aae 100644 --- a/processed_threads.json +++ b/processed_threads.json @@ -1 +1 @@ -{"1771693114718505586": {"nums": {"5": {"processed": true}}, "metadata": {"subject": "atlas: what's the purpose of graph data?", "timestamp": "2023-07-17 18:23:28", "sender": "Sean McDonald ", "receiver": "Dev Agent ", "in_reply_to": "", "references": ""}}, "": {"nums": {"b'2254'": {"processed": true}}, "metadata": {"subject": "test", "timestamp": "2024-02-08 23:25:48", "sender": "Sean McDonald ", "receiver": "", "in_reply_to": "", "references": ""}}, "": {"nums": {"b'2255'": {"processed": true}}, "metadata": {"subject": "joke", "timestamp": "2024-02-09 00:07:57", "sender": "Sean McDonald ", "receiver": "", "in_reply_to": "", "references": ""}}} \ No newline at end of file +{"1771693114718505586": {"nums": {"5": {"processed": true}}, "metadata": {"subject": "atlas: what's the purpose of graph data?", "timestamp": "2023-07-17 18:23:28", "sender": "Sean McDonald ", "receiver": "Dev Agent ", "in_reply_to": "", "references": ""}}, "": {"nums": {"b'2254'": {"processed": true}}, "metadata": {"subject": "test", "timestamp": "2024-02-08 23:25:48", "sender": "Sean McDonald ", "receiver": "", "in_reply_to": "", "references": ""}}, "": {"nums": {"b'2255'": {"processed": true}}, "metadata": {"subject": "joke", "timestamp": "2024-02-09 00:07:57", "sender": "Sean McDonald ", "receiver": "", "in_reply_to": "", "references": ""}}, "<20240224184044.52D9CC0475E3@pve0.localdomain>": {"nums": {"b'37'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 10:42:03", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224184044.52D9CC0475E3@pve0.localdomain>"}}, "<20240224190145.35BC8C0475E3@pve0.localdomain>": {"nums": {"b'41'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 11:02:40", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224190145.35BC8C0475E3@pve0.localdomain>"}}, "<20240224192518.AF838C0475E3@pve0.localdomain>": {"nums": {"b'47'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 11:26:18", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224192518.AF838C0475E3@pve0.localdomain>"}}, "<20240224193533.D477AC0475E3@pve0.localdomain>": {"nums": {"b'50'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 11:40:24", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224193533.D477AC0475E3@pve0.localdomain>"}}, "": {"nums": {"b'49'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 11:40:24", "sender": "root ", "receiver": "", "in_reply_to": "", "references": ""}}, "<20240224203127.E441CC0475E3@pve0.localdomain>": {"nums": {"b'51'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 12:36:28", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224203127.E441CC0475E3@pve0.localdomain>"}}, "<20240224204359.C200EC0475E3@pve0.localdomain>": {"nums": {"b'53'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 12:48:37", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224204359.C200EC0475E3@pve0.localdomain>"}}, "<20240224205422.5A2D2C0475E3@pve0.localdomain>": {"nums": {"b'54'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 12:55:07", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224205422.5A2D2C0475E3@pve0.localdomain>"}}, "<20240224205709.25EB1C0475E3@pve0.localdomain>": {"nums": {"b'55'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 12:57:59", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224205709.25EB1C0475E3@pve0.localdomain>"}}, "<20240224210054.2B362C0475E3@pve0.localdomain>": {"nums": {"b'56'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 13:02:58", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224210054.2B362C0475E3@pve0.localdomain>"}}, "<20240224211700.A7B06C0475E3@pve0.localdomain>": {"nums": {"b'58'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 13:17:27", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224211700.A7B06C0475E3@pve0.localdomain>"}}, "<20240224215433.CA525C0475E3@pve0.localdomain>": {"nums": {"b'65'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 13:57:48", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224215433.CA525C0475E3@pve0.localdomain>"}}, "<20240224215922.091D0C0475E3@pve0.localdomain>": {"nums": {"b'66'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 14:00:32", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224215922.091D0C0475E3@pve0.localdomain>"}}, "<20240224221945.81615C0475E3@pve0.localdomain>": {"nums": {"b'67'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 14:20:39", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224221945.81615C0475E3@pve0.localdomain>"}}, "<20240224222208.CF5EAC0475E3@pve0.localdomain>": {"nums": {"b'68'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 14:23:12", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224222208.CF5EAC0475E3@pve0.localdomain>"}}, "<20240224222508.293FEC0475E3@pve0.localdomain>": {"nums": {"b'69'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 14:25:52", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224222508.293FEC0475E3@pve0.localdomain>"}}, "<20240224223028.EF174C0475C3@pve0.localdomain>": {"nums": {"b'70'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 14:31:23", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224223028.EF174C0475C3@pve0.localdomain>"}}, "<20240224223408.82F06C0475C3@pve0.localdomain>": {"nums": {"b'71'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 14:35:15", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224223408.82F06C0475C3@pve0.localdomain>"}}, "<20240224224211.E047DC0475C3@pve0.localdomain>": {"nums": {"b'73'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 14:43:04", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224224211.E047DC0475C3@pve0.localdomain>"}}, "<20240224224454.7D07EC0475C3@pve0.localdomain>": {"nums": {"b'74'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 14:45:58", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224224454.7D07EC0475C3@pve0.localdomain>"}}, "<20240224224801.3C8A9C0475C3@pve0.localdomain>": {"nums": {"b'77'": {"processed": true}}, "metadata": {"subject": "Test Email", "timestamp": "2024-02-24 14:49:22", "sender": "root ", "receiver": "", "in_reply_to": "", "references": "<20240224224801.3C8A9C0475C3@pve0.localdomain>"}}} \ No newline at end of file diff --git a/shortcode.py b/shortcode.py old mode 100644 new mode 100755 diff --git a/start-config.json b/start-config.json old mode 100644 new mode 100755 diff --git a/start.py b/start.py old mode 100644 new mode 100755 index af986189..2b56a456 --- a/start.py +++ b/start.py @@ -1,9 +1,13 @@ import os import subprocess import openai +openai.api_base = 'http://127.0.0.1:5000/v1' import json import argparse +from dotenv import load_dotenv +import os +load_dotenv() from tools import update_team diff --git a/static/content.json b/static/content.json old mode 100644 new mode 100755 diff --git a/static/favicon.ico b/static/favicon.ico old mode 100644 new mode 100755 diff --git a/static/logo.png b/static/logo.png old mode 100644 new mode 100755 diff --git a/static/logos/atat-board-art.jpg b/static/logos/atat-board-art.jpg old mode 100644 new mode 100755 diff --git a/static/logos/atat-board-basic.png b/static/logos/atat-board-basic.png old mode 100644 new mode 100755 diff --git a/static/logos/atat-board.png b/static/logos/atat-board.png old mode 100644 new mode 100755 diff --git a/static/logos/atat-glyph.png b/static/logos/atat-glyph.png old mode 100644 new mode 100755 diff --git a/static/logos/atat-wide-trans.png b/static/logos/atat-wide-trans.png old mode 100644 new mode 100755 diff --git a/static/logos/atat-wide-white-trans.png b/static/logos/atat-wide-white-trans.png old mode 100644 new mode 100755 diff --git a/static/logos/atat.png b/static/logos/atat.png old mode 100644 new mode 100755 diff --git a/static/logos/email-example-1.png b/static/logos/email-example-1.png old mode 100644 new mode 100755 diff --git a/static/logos/email-example-2.png b/static/logos/email-example-2.png old mode 100644 new mode 100755 diff --git a/static/logos/sl-logo-art-square.png b/static/logos/sl-logo-art-square.png old mode 100644 new mode 100755 diff --git a/static/logos/sl-logo-black-square.png b/static/logos/sl-logo-black-square.png old mode 100644 new mode 100755 diff --git a/static/logos/sl-logo-white-wide-trans.png b/static/logos/sl-logo-white-wide-trans.png old mode 100644 new mode 100755 diff --git a/static/logos/sl-logo-wide-black.png b/static/logos/sl-logo-wide-black.png old mode 100644 new mode 100755 diff --git a/static/logos/sl-logo-wide-trans.png b/static/logos/sl-logo-wide-trans.png old mode 100644 new mode 100755 diff --git a/static/logos/sl-trans-glyph.png b/static/logos/sl-trans-glyph.png old mode 100644 new mode 100755 diff --git a/static/main.css b/static/main.css old mode 100644 new mode 100755 diff --git a/templates/index.html b/templates/index.html old mode 100644 new mode 100755 diff --git a/templates/readme.html b/templates/readme.html old mode 100644 new mode 100755 diff --git a/test_email.py b/test_email.py new file mode 100755 index 00000000..63ccc8f3 --- /dev/null +++ b/test_email.py @@ -0,0 +1,40 @@ +import imaplib +import getpass + +# IMAP server details +IMAP_SERVER = 'localhost' +IMAP_PORT = 993 + +# User credentials +USERNAME = 'emailuser' +PASSWORD = '1234' +#getpass.getpass('Enter your password: ') + +# Connect to the IMAP server +imap_server = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT) + +# Log in +try: + imap_server.login(USERNAME, PASSWORD) + print("Logged in successfully.") +except imaplib.IMAP4.error as e: + print(f"Login failed: {e}") + exit(1) + +# Select the INBOX +status, messages = imap_server.select('INBOX') +if status != 'OK': + print(f"Failed to select INBOX: {messages}") + imap_server.logout() + exit(1) + +# Search for unseen emails +status, response = imap_server.search(None, 'UNSEEN') +if status != 'OK': + print(f"Failed to search for unseen emails: {response}") +else: + unseen_emails = response[0].split() + print(f"Found {len(unseen_emails)} unseen emails.") + +# Log out +imap_server.logout() diff --git a/thread-reconciler.py b/thread-reconciler.py old mode 100644 new mode 100755 index a6229860..5893dbf9 --- a/thread-reconciler.py +++ b/thread-reconciler.py @@ -5,6 +5,11 @@ from datetime import datetime from email.utils import parsedate_to_datetime +from dotenv import load_dotenv +import os + +load_dotenv() + print("Script started.") # Fetch IMAP server details from environment variables diff --git a/tools/__init__.py b/tools/__init__.py old mode 100644 new mode 100755 diff --git a/tools/__pycache__/__init__.cpython-311.pyc b/tools/__pycache__/__init__.cpython-311.pyc new file mode 100755 index 0000000000000000000000000000000000000000..b1b55eb4912b851c71d83a3ea0bcd58dbef55e74 GIT binary patch literal 137 zcmZ3^%ge<81k18-rh@3lAOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnFB$!m#F9jP z#}LO5{gV9roMQd>_{_Y_lK6PNg34bUHo5sJr8%i~MXW%@AmfVpfy4)9Mn=XD3^1aI H87Kw-C<`1Z literal 0 HcmV?d00001 diff --git a/tools/__pycache__/update_team.cpython-311.pyc b/tools/__pycache__/update_team.cpython-311.pyc new file mode 100755 index 0000000000000000000000000000000000000000..68ddad0136b5148cc5499f768062b7062ddd2776 GIT binary patch literal 7632 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