From ebec73a666b317aef2ad835deb6a6946e0f44b19 Mon Sep 17 00:00:00 2001 From: Danny Morabito Date: Mon, 2 Dec 2024 14:28:00 +0100 Subject: [PATCH] initial version --- .env.example | 6 + .gitignore | 11 + bun.lockb | Bin 0 -> 56363 bytes package.json | 34 +++ .../20241125122247_init/migration.sql | 29 ++ .../migration.sql | 10 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 28 ++ src/httpServer.ts | 247 ++++++++++++++++++ src/index.ts | 24 ++ src/smtpServer.ts | 100 +++++++ src/utils/index.ts | 16 ++ src/utils/logs.ts | 23 ++ tsconfig.json | 15 ++ 14 files changed, 546 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100755 bun.lockb create mode 100644 package.json create mode 100644 prisma/migrations/20241125122247_init/migration.sql create mode 100644 prisma/migrations/20241126193747_remove_mail_queue/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 src/httpServer.ts create mode 100644 src/index.ts create mode 100644 src/smtpServer.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/logs.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7af7356 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +BASE_DOMAIN=npub.email +DB_URL=file:./users.db +SMTP_PORT=6587 +HTTP_PORT=3000 +LOG_FILE=/tmp/nostr-email.log +PUBLIC_API_BASE_URL=https://api.npub.email \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1ff2da --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.idea/* +.vscode/* +/tmp +/node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.DS_Store +.env +users.db \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..57b31f4bfa68883de975a692381065d4421a52f5 GIT binary patch literal 56363 zcmeFa30O_r8$W)kLx~2J3Pnj#n&)U9C__?-h{n^PxpSIH0~M7F6(X98kW?B-(yWxB zL9-!4AtEBl?_H;}+28A4a_{f?KhOX9S@(JGI%~b_^I7kD*SpqUd+&21Bq-%g_L8!3 za+4yt`3Tr}yD@`ExVYOo+dH|CB<$V2T&*b*KGMwe1Og%d>Tvp517GG#61=PC-ez1= zktx%Xteum1LFk=mLdcDf6J5{3-vRg-kR+1W(&B@P)7DRd%KXlpiFYJ zws9fZ5#pv32+UAVA^A}x97%q*?k?^~U&2Tr%z}D07&_|58gkUnG{{l=bD(EIdf-L& z_|oj8kSQ(zCpfu!xO-7#2}GKG!2m*dAIK5j5&nmGEg?sCjsXVMlU)4CPSykhCv-j+ z%Jh&UyWayC^=Iqu?(9S&5MDxigl~hK8}iGvas*UzL75($%?tS}C?h`>KpFKj3^}qx zo_%tBk3o<6?SL}srx|io4ut~Z@q`@Jmq9%mCkfgko&d;^J#LVLiNP*THe^p10^u{~ zWWW88^FVzQx2#Onb$;v+$h>}!DnALOqAjOy#*f2bZJ#K{K6 zS_Oh|nE%sZu*fcdYZAo?bU9D}(}Kz1M3g@VJ?d}MJX$|w1qqmP!U&WRjzo5IvbQG? zDE=PcT>`;*{$x8lRDd)H?Wpqu7?gUUjK*z8vhj9+G8U&PK!)0_gZ_}cDzttnWD>~( z+DX#tcP*G~=LtFDc?-@$<7A`hNwoSWw0caBl{bMNjcXo_AF~6)e+3*&**9Jchw)^9 zEofdntSOFYz`=_q`)3lGoS!i$BmMwKk~Q*yBg7qAF$dr?n%7{+k=_S#G~Q>6C)c-v z@FWj5E^Hh&-uCur;W?2=BqarTf{@6h-<4>14=*ROE5yA$ndC!qqmY*@ndDL-Y36c!rpv#)YxiehVwM5DlQ{~zWPiK5mx^nlaO*Xk8La<9}`;zT~&t@kM^6>VxIK<`V za|A}p+Kj!NaQl!`QJ-E=)mqpdtH0%L9Ao6nJUin(@nf%4MoN#|d%ms_l~9 ztX~$o*PDgkyn5-W_*-{Zv!^9PZH6neUc^3>=PourzbL0eM5wIo7GFX3*bPO=(8DFW zcAQdJEw?#%^WHJ9u|mr zeBE)0%Mss|e1{ep@r-=AM!%g~(c4>m?~q6StK+;Uv)|Vb7)Mv}@ZOn~)^N=K^p3ku zhorW|h!m=+%y3t&8QhR~<=}^msf?T7DmH)v!IjLR-?dvBioqES^-|*k;_#sXF2O z>4b`#uTUy`U-V9w1V!?tYQCC$DW3-iXS4~Pccq)r77=Hg9HaNuGwSfjs|_3y{%p6M z)s;mzSyWc>>o|Ve8BEkDT=nJX{^LU>-G=g8wL(NBmRLra$1&(OY8$OQ-E%gT=gLQ$ zHvUa>?0lDD{%=h1-tej+G<##9<@CGjSbA?OaG9ESk1z_g?XO5l-|4Hqi-r5KSJC>8 zPD97X9HL>@FZcHlWH_D>dQQl>df485zUCFL+bN-I4o|mm=Q-$^gh-_KyiPGy%6D)` z|9XzxGMFhJUc(go{Qii5e#ht3bA9xT4@!1$CL9PO$UmHSvj&S(NAiWQV{AE2E0Px! zFNnOPkS-q0!mMq(_LLi&1vj~JXp{ZDi#;<+I72^Wc_lqT72Zs_G3O@X8CHD-sucXkCnx{RBT9tT4z6vo^&?4km8Hkf#HWM zHrIN24V(QHhn1K2*;{Vj(A+B-c4e{oq6a*OyVhGOgqx}q^e;Kju+O@w+@bSCbICq& z%Gati<^k6Dr`J>B@4eHjSyns#aByL-m=RypIWw~)(#yA1{(|zmk0pq8Gb;AejY{jj ze_gzLh7mR{Y#h}Fy3tDmDq{C+^qw$YsQYn~@kzqO>#>`sg@eVH?IqbT9KIGJCH-Nb z_+Ti{-l51|p9_NCTP{mCsa(*}VW*Fd-EE&CR2lPufpvEg?;bIKm%x_}w<0-SEc$Ca zC$!eMj6I#`c1|og*IV=CW4F+&^|w_{m%}mPKP7q^S`dg}PYs4+6598soG^R?;8z2l z2uRppr<^eS9QdFPcr+fwG1YNlcoX=r67a}=IGPc#dieO$gtaS#54yDe5df?IDHzx8 z6Aq7Tm}hG(1#kk$snPlpbTX?Qe- zsU{4+8SvsXJR18{eTd=r0A3dGXxymock!12s0Vng@2QRf>;E(0RcUw>e^X5uUI{wX z06d0+v(c0ihW7zHn*XWB4MvXPF906RKf?bG2Zrwl{Bpo!<*9OD?H7R4(EOo!Lv>U2 zA%-^sfG)28-<|(cwEi(Z%s;=%vG%tCkFB3yVeonkKOGhw7C*>$5KdD{7+xFj|7HFI z0FU?o@6KN};5EVj2#5Cn-^KI8{V|$9fKull8y717EMe_j0FU-h)E0}OpW!eahCd5< zy#IeUex3qe8#n&n#V-Y?t)<~H+oqCZ_EP{a0eCP2-N#HhVfYfj%K#pgVJV|~z$qmR zKL~hi|3JXs9lronpx@mCLcHNc~N2Zn;4IZY{H=(ft3; z-iPeN`j>`_-df!Fr^UHQ^i8lZ}xu#yb9n^-M?AC zG7xkq{t+IqUeJIMbipGv~|XPo!_`h$Eh z)fm9=c7WH0{xSberEk;*!)F2>ogYvg8oMU?FolHSzW{zYt^eP#0mCc7dq;V|W96xG zVD0??FHOUv*!|t>cQ)X)Y2!!O-}V0>;KgX;M{dLHLZ5$@uy$O0-_I{-?7tg7Mu3;3 z^*`0Mi|oSs4+6X<;IZ=W`tKUxvHb_T2Sxn9N?3ny056W~|5rFnkKy_FC;f-{4Xwec zBn-b9@M!;?YTRMm7(M~;==_0|G5xP{to>8KBmbfPk?*H!2ZrZeF!}remC^eB-T8|E zyejk$&?)zg+G71z0v=tzk=>~7ckLes{0iLoF<#Wi&l1*7Y2oDiAJi`IAl|17{`_D^-}$Of$cM>zby>7QTVd;b_GHpZXjSli8jSEJ28+WUSte&PX--9KTR zKkFCMVQrrP9>qUezo_r2?qL|7NAUanqq?ci0fsjOycYETJ6P!E+x*1>9-Du(|NUjve3sv_=2hKhjMlVdK{Xye5nv*^T6P&rdOc$M!FT$9Uo6PZQRz9Pr40SYz}Y zY$^%E^9y~y|G~!aJ9r1ctJB60j}m|T`AY`ivFjhQ3B~bL5@vrp;L-UDt-XJ<{@50O zfBuffKh=cwzZLNK^V?K81pd-dJn*1%kdL&av?Ltp%Avdz%71D0t%&vi0orLmJ1lml z+CPTZ7XBVTsBWrj2*d9JJhuNM|4%hx_{%u_RM!sb1H-=tJUahkScWMYfZ@eOzQ4af zb-(LB8^B}pkH-JI_-w$V^8>;n-(z;c$Dbyw-CMw;=l2K?x6)Hi7+!J7X$)%Wik75*ZkgyFMjc;q`|1GgBnShy0aUyIc?SGb|{`Ejm|N0;Z2k)+^^S%Lclu(YwV*-NO zo6_=)kfVfhgx?H;^yVO_{ZGbN9_Yq0TRklc_%7BLOGh3U=WmoK~Q-Q2udhNyy3Jwl9um>9PvbfAio`; zm17}C3FSzC5EUT(UCspc$Ee!L9QBh(E8}y-p9F&VPSNxzN9EJBGRjdonU<$Oj?&-d zs9h>mJDH>LX41+iM|Nb>$|y(Wv$QfkM?5(oC_e{+?92s0{x1YUaaan1@K-^QzMNLB zfE=a2%TfDEs&+C*_SMqLC`bL(fgrvH5Y+w_2&!)aK?&uk{x%4zzeCF*qwYNWCPj{H28eF*_jJ2bz#r~v6_Ir6_A2%0Z_5QO`)KYXR9qysT6sm*NvrPC%-S0(<};RiC45m97Yx6(PDUr^UK2Sw zgDz2$*Zw(<7ws8XB042JFcWbOGiC8`k21P^7DxeC-3b!cTF{Hm~676MSE8wju*ugmWUr-zdR+7W9#wByo;#2vz9aD zvX$^)vYXyo??p_K9D=1|+irABi&3f1)fH1*aO7S|)dT719oJfpt|St#=(L%y zyHjnOWA0Z88TNO)k#2df$T(hfO~n#1TzAv(I$HUGv!Wc==}b2E z9c(6ysj0s^tT`{ywLNQDtE1KP)cNlF7n`Js9ZfcjeQ_acoY@b@%ZQOePP~wGH_h;- zXGf-JUT#R5r`96pS9$RU<|=FMh_zUr$tZAqz5bYg^6(K>9pB3JN)N*Zil5oI#utAM z-M+)k=2Pf494`}|_sHiOryva*v!bo4(aa;aReTl*1P&_aIArs<-I5t-iksUwbMewT z^ZU7bPVyd>>#kWHI?eCqre)m4-Vw(InNL;Vc+uGsOT@KighwjPFW+xXG|~^6YaIAw ze@*kY!y6mdYMmr5l9HUQVYsXIxs$@F{X-cWubeoT%2; ztKa+DGsRzQd4F2^i+amO5;2+jmoNyzn)# zZ;80nrE_o7K#6vGvyjBc{lNjJkC?XI)EH2dbP+5m`*Olae4Xe#))$`7GQ*f-Bg9qv z#??xRA@_uBZPvQ3CLBA+jN@hh#tQcKD>UunxL6{{r}K$&&}#1h%VQ5hb@>V7JYLuYUyPokad>Z665&NQI=*h)W5qawe zOuLU!VpidJ(Rmh2#64RUi7bBd!vERW8VNot*T89q#V9kgMBYC;9I^L-!qMGs*Y1@i z*(}Z4;#<-g>$z%ob)L6rKI@~PCZV%|dL<87alGieb66sV-;QIo@TyzX7?P$`?I%A| zyRB>9=@Qm6_Y$?)qj;_?R(CdEYLz}jY`mj1jI=%db@+pSLmfvZ%L77%fkdi! zoxhPqSgA+MDPbn{^@F{|d&>`a>f(6kV5E=}x92)6yp&;n{3v}VzlHQ2&MPmUD=360 zsNM>$WfhKEE#}v+FP#%W65rW;+U-{8ctdB|8tFHMWt}Ob4eT|YX%BF`_-A-T!r5D+ z^^(_LHVWQebjvgHUBFIwh~a4cQyR+NYDesaFIhR&FPWA$K3_`V z;9^ett+A0Sc2B6_c;Ra*-x6{9%$p+1;;(E@o~Gq0Vn>`C>TSb=j(sTkmkZyx&;C52a65D!4pY5}l%# zX1xDtvu%Ewpty2AEAh#ik&5Dhk1xC3NTw54INv^h;OS+3y8BF1-Lj{j$mwITAFpeP zR^+n-jOcD}4 zKZDV$s5(roeC1N14Msw~{9_wyGne9c=izxT&1-cgPD@qkC_lIM@ye{);Phm3Y%T>^k<=R;@!^fYbC?GWW2I>8!V_p9pjHFzJrm;X%Kt z$JQIXj{cU2T5q@t`*P>pmYBmx|3=`FO^>Cui*Yv7{*J1!7t$4Pdb%?!=;%j0v$~t+ zv4(#UtQ8LCeAnFB{8n<7)y#{nTj$|;7k*=fIMZRQYN+d0eh|N+OY*(WL}G==-h^W3 z*2BW1v4Xdcym9d`+jjgz=d2-*vvb~5R0-6pI3|jIOfB&7lFmc5I9>rf@32{) z`EY~lRmr}`0m?b7vIn=V-^ZgWUGe^gUB{(>#D#8T9@5FK+Sb=ODf^g@^{uOAxfHA3 zdh2mn^2-m6Ny-{HUO_zX$5)qJ3fG8U_YiYqylJ(k;xOm!F^@HV^fo&!^<{(>YX`Hw zop#~r!I++>F9nzt)>*vlY434tZEkuISaAJ5tDXstcM+b~@=}Cil4a`fmVMc~bOs*# z&Z%Fzch?ucGa1&$uX}wV-QAqTP?0|*Ez!$XyfoRJ?}MPN{gbHb`l>Wtw)Y7$lIG)h zh48#SnRivsjH-$i%{ygX#i{mTsCl+}x0V3+`J9vj=6cuW{PPo@@;YfJ`VP4Jd#zcV zvoe%;^^Ux+#>M-)hc_Fq!(T5J<9V;l*5_Zx8|Nd)GIQg@g$4r`eDtsSykSpUKeQ-d zLEYS9Y18-vV+O^2o0RM4ZkVkpPVcO*JM7ErU{<_Y& zxw->$al9gU-sx@;ukRPMC_k7hoe;AoH2Ty*)|gd{y|L@ZwQXMRGx};ar%>ly!HM}H zbD7_TmhQ??Y@F-9N+~eZ_3O+Pqesu-&u2^Uyr(JsjO$Ws2!?xCTvs^5;2yn2#{d2X zbC#6Z1~H6Jywcbs1evR}dSX47D>dxQ)0Iz@`P%o^vuWc{b702!X-Wpp-lce6%CLmW zV=122wVr!-x7v?0jm@D8bb5XxI=<$5QN*^h)@O}Qoz^0sNsgB_LupVu99-m-dc z-^9Ch3d-x4v7qvaG&<-4=xcP_XnDIAIHioH~L<)&8tn}7xGBCR`f zW7~LJD^;5hHs31fc%!l4&4r^uY=vwu*Pit=seB;Rsfpti`^F0Ecyl)M7S`=1^Y7TX zaKG8w=bG}Fj*`-GW1?5xQre|OTHK77)U(Ow%2o~8j;}*HtBM#iGE;7krbzn5$gA<2 z)-A&EF2nOW`}p2$6CPMNF|^6E){t$*)&+@cx#M?_XDrXVIIev55ucz(wc0U5^2WU3 zb8U%wiUYOGz1D9$9kNcZ%vB^XYvXvu@w`gm5(c3=Lg%eFE|+9tZ5sb$aQ zws-sWST~J4=f7epaOm?B*8|UqCqsI24ey%5%{I2LOW}E+m&&|MjM-W1kZ~>4&1^SI zLtOtrY^SLfZ&<$%S?nI&*_lOyFXg%P4xM2Um@D-u@#7MF4tZQ$2tc+OI~Igc=03qpw`-hikXqC z_!QPW@@Fe5z1la+czOF-wxiX7>$~qHQult(_o1>s9j{BU1B^G z&^T5uk+ZzZ;FFV-r$}y^G!KjZK-!-6uQF?TZYmdF%je8iA1FNV_3@ivQ%zo$K?xJ} z0`V5cA{?(AMhZER^=_NMuCi2b-c{=s-F7nSS!J<~y_9`Ia+K}i$W^Behs|`a&pwi6 zRB>wGp}vE@)0~PKa(w4|zx~48`G|Lw?;CC$FM6MZC1Pr&Mu?S(YBuAh>VCt4X92`J ziyEp2qeXU~R2R9ua0Tm%j!st}SN=HXk5$V8<`ZXoNE|hN@mhENhzm2jN7=!HI9>&e z6mlX@U|~>mpXaM;?)6~|+{5H@2A)QWXN;O2$M`(&hD2@e1LMAi_jV~bZ_tc}I-Dq*CM6TR63-`>16 zu|4^WbHx^gmS0&n=dG&?E>_E6S&yj=|D+68+ zXX;5~mnMuX+0 zdu_;Fj^lx(;nmMSMpm=31!=|Bb!T+w%Y4cmI=*_zv9oD=ni!HkG?ulFrM3>f3tXy& z<6VxCLQeFxF%w^!dpvHb=)S4Q3+Z{1(k7W96lgx+vwc-q#Lx_8Zv%s;Vo z)BZzxE#)bTKRbIi=Yuef#XOm#3LqqsVv4j(rw4Q7tcD`pD7QRWQig z=kv`wKPY#&$v^$#ZDa!6D4Nq3W z@nY{w5Iyk}_t3V98V&MYdq!Tig( zFjB~2pDe2)EZaEOggO4yS@K%uy=`qruWa5^*H^yeL8rUPp&7RalpI^?W;)$b<9&D` zmZDYvRJ_bq?PlxsA#S#f@{{LvB9&Jg&%0&ghmFDhK0?X|H{EiIkd0vV`lPKXw9`UX zi%a;(D_t3;D^13Eg3db6rINF{%@&L?etlQCWRInb@Tk~ho{L(O=Sc!nFmUSNd4~>@ zRa}}+oSk7ZH{;`*uvLoe4_K_%R$TX5e4T%I^}|o|S>i`~y<<(??)Q!*iFFB+334}f z++t}uXDN|Y;nWpH^#g$kJ=?+FS0Q@h$phyzBwPnvYUq1b*P6dy0Dna4a!|DY$ca(` z`IfN^x(;pi>s+RFG7oN7ek@n)UDa1V&_ddvSh0Py{jflTngxy*z30Lbk*WTcu4sli z^VufTH_zJ>Njn=h$Ih>BJIcDMv^%ba`SaCP&mNEKxJVXi6})*;%_2RohTp=;*+_6) zTM8aPQ|f8+hMrZg#7H40);;?C*k+-*foZ^}y8clILG^{XB1#f_dL-Qf+ti@Z|kGfeG=h!t(|yt=M!eC?NXMLc@v` zUHWxLcWm>Wz3a$)(Hl4XEY;80l$`u3z2!0A&L>_K&%`b4wyy6ZlaI!nzrez|Aiw_; z$3?2WlkXYOdp;}?56M0BK6ylkJ*bP>Mz6x`oYI1rrWryxcIy3CHXICpwEcKllby-O z={jR_S1Au$DgK30aq({!&U@T>K=pR?(ukQjUIUC2a^i-dbMpprucaq$XIwpZjR0xK zNs~o94xc*=OE#?knC-SGg||#jD#ACl;&Ja_M!e1HF{yJ=?03I<95B_{>o%x|{zeXV zj#`c9wXHul_xP?-|M;Md&(Cz51g!Y-UUTIB4nMO{sUza*ww$Jm)Ss3)qB z*&RnB-%Xz(VHUIIM392x!!IO6n3~I@u^@{%NoJtD$fPXo5S{M4PJ~Mvo49j*}E3c+rs9@Xeo3s zsnX{B)kl%*YxZ{Sl2%zzwPL*X<1BrPx~sm7YFAb+*tKuTe8v>E$gd$|%OwLd>8mSw z4Dz(w+E!o3@uK&#SRy7LavKe~?fxjIaZlJM6YTW2uI%oOvE*!5RMhZEREb8uD_tKFrdcUfs(5}#v z@fl6y43DZW9c?QA#L4kup84r8rjRG5J7-%=BrNv)K%VvGb?a$+fzOhW!G~^sE}L8z zlX12l&zruje^vFol$(RuN6+vY?psv*a>svY-xApNe4J>4;8N9(0Un*1W{ zrZbI;$$e^jmi1U<`G4-bC}h0lp2phNF6(+hop^q_*qYfJVtHSE!ubJx4+l#`*^zl) zzwY-u7;aI4rXzR@S@*s&1qqcj@Q=C7= zMc(dG(i}M!pM?H4Cl1rWueJ45$jnmR|E%ocY%8Mc)w-J;O7prPHEAj^A24l%%yka?D-ypBOrp{+z>7iU@ zyPa$4r<7ZC#tV`HaQ@wd=bhne8hJF+tTf&JSdB6zV`il7LuEcV^uwIL#V3akxZs zu^V5$=}95q-YSN$o4K+(-EsC>;CW5js<{=$3pNKE&a_)%@u2JU{%ebPGKucGl&g6; zMRPxHyrsNBJO9+-%HH*wW?OAp>~9&)w(*|zzDcuwWNEtHH5~6YJntaicH!`-_HFat zUo&BkOS{V~R7W8kV>uad`b6ZMB3)^hBkS`UaWwWiBr6oXFc=P6A;Z>(M zcwC9Vzkl0~=WS-?Shbw%P~XX;PaQe8Ykr7v)avuO%{>zNXsx!c;8DL@>z z?h%!28I^3XZWT)!bAZ=p(J=k?CW^^Y<)`C@*L~=;Ha=i8t+ZI`%qWfXgB70FFfriv zM5NKdd&Pt0Pfqjs)XRxH@4J!JWVrf`5YuNt4vtL!8ArSyTeX5ZSv7OnzZSNU> zQDfeMnY)I>Dg8KJYdr5mb>AytJ)zZuqN#jEbUXLGf1)SP0_NS(GhcaE3Ue$Knf zo0hVgOYe){J+@-4TrC}Kbc&ZN>MYJ) zTRgAGsX!g^s};V{hwi!AFBRs@`7l$=s@*q?VUyrOLe1dnlGdOHM?$Y?uVb-U!^4_g;7F20j-zLA~mf18LPaH1^&udYB%JP$}MJK(_ zeIl3Xhk<=79dDNV>&&a!kg#d-&V($TGLfb>UI#p{!|v*QHLu(rW$&(iY&uR+i&yRCX7sLq z8&OtGa*Y2xma;`k>H1w?g&o2+$$ONZn&qwiu+C*(^f3X?*=o5KEjBn_M?9~L(N%V$ z=*xyGFV_)KYZh+h*&pjgc0YLkuC?A!OaCadpvf_dX(jLNwa>oHFp08$8c1&B-S*|S zV$?JHUb9P!_TqS*@VpM3DS6^~w)FL}0c-4*oK{@sc}Y#Jx9!8VTuNfn`I_(r(=)rH zl?S=`qKBUd<$T_I;#~b0Nit`y@qKeigP65zalAY5yr~VZ{iD2Y-I?Ro)3kdQ5BD{; zPGK#n6NNef#mcuWSz=q#UmQI)ur-WPp-OX)Te#}=@JsnM5`+6XH&^p-D0*@m$Loye zz1QPNTx-SNp>fc0lqMT?oLuC;cVH$tP2ebFWg(msu&ig8^yYbOeCyaYX)jCd< zt?avnW>{C-Hly6kUpK6VoTX&1b<)vz`Tf1P*5^QJ@ypSwTXvDx_I z9J-B{$K{gmZMwWlPxeCSU~fvu63+t`FAg;w=XT z0;}|GKh?XzXG3tjo_O9_PT>VooJHsB`yFaSKKV00;ECvs=XqQCCU;ftaPIA-4EjAQ z*xP~=$4kcZ zW?s;F%TL+kVS6)U!t;&&U8xxxyeP&za&wxNy^iiG(byF7a$1_T(vnTAj?UX0MC-&m z4!&sGyxFCO)69KPOywSqmxAZrASY;%YwV}2C%mQBU)#Urxc)$Pd)PMC=Sq&AUyYwT zcInSCznG}{X6Ma{ozE18&&bH@)EyU-mD5p+TzEId3?FCScwTG0nY;BCKmHWV(Cjg` zZlZpr;PcL^a_%ip)DPS46qX%U^qSpYwUJ9eR`rW_p_V|=v*O^Vaam`3+d1Y}FFzB8 zzt8f)^D>&Ho|h>KSa6D)&U3Cr+~r#HSH6nZN=B_xr-_@TzK=M*k@N5Z$0AZj$5pE( z-75VSlrf>fywUSAr%JOG#Y9bTe(=Tf-r(rjTr;ynmFZrc!@CbV=t^4yoD0O;V&oGx zy(X8QN#&oR)Lz;(b5FexMQ799Y!{ub$o)H%zofGs_g|X2D+~X8(htwOE!=m|bWd&5 zqeq0n)Hq>Yp>#?pC!eQO=$d`ph1GQO;-fOxSMH~%_6R)W-n>QQc)BoO+_QPh6>k*X zSQ+S=nS`^~AI~e_Sftsp{AN3S^84wFcS$_B=iHZ;uzk8UL%_1cvU}I&yq8yA%G2w? zBgQ{>!+4EguZie~aSPYkDp!p-x)@LDCgOO}_kgiP+_~SVHYGIEa@W!Q`?gmO9@=R* z)DZ325JgaY=vbBR@88NHId{>fYe)NS?rDVc(TR$v1#;CKqzpNXm~bdp%N)V+24bX; z6W?rO6*}s)Iao!&xNwD|xR@oZ;V1Ln!SFp zqo8PmBSUR7dwjB#|TC6VQYLR%XIi5|@8ppdE$NQf@;&_8`WJJdGr|#KZWg)08o$td<9}#RX zRX|yK$jdkHC}$nxsw0@3(!>6j-CMh>fg4DJs;&?;w zysepGyYwINwLTZgR!wp*dTiQYZfVwa{iSJZy-^mU)Y{+^^e>+c1sI5Sy}4AmM6UB# zp`%q9*PWs*qvpa4%U$>5cti2LtowuLLH&`qoJWumQ3;=XR9vL7#+Ki@cG5Px0|$MX`-Zn4?$ z>}@Z{#{7txQYW50+8;XNckK$p+YhhL7atTkv0`WPy~8RDxsqv7t6M~*3yvt>@_YNH zdEV71F~`SSPN9E81$%B3f#;1n5il@&nDza-$69?XJ?k&(KIH9qA}z$(E5}tDx$CQL z=NX4w#eJ`w8nU(uQSM(<`|@`FY$pr;qXKI)zi5X?;hz&k;(4!i+HUn8vRa%aTe)$% zY@?#?CK$as`lMbhTbb#tLo zseKGHOnO7Mr(W)T#%9hToYT|$(TbpN8){s?IE8;nDO0v=P@Vts?AHRSINm5c@BNXd zRi-|5-sOJ$LuZ^VLU;t|@(Oe5%Iu2|m(5Z?<@1>w6`D)Y^st@(V!V&DnM2H|P*E*^ z-045tKoHvB?hjy5*~Y>8x?Z;7RY`IIA^<0#V@vVM&H1`bsEsW#5=YsRcFRxr1&*sgv|3W2>_XwVM&Mcp< zwU_TdiZRR#seiCC+xu8^gz#vT6PahP&l|@+rkVkAMM)B!B7?c!c7rD}_QxA$JcxcN zX%N10@%*GHy4k?{C;joj9}oQTz#k9%@xcGr9zf?ul%`K+fByf+1Ajd5|F8$pet`A@ zY+pdGfLk{L_HP*Wi%kC8h^YQgn)JY*BmAF8_~U^;9{A&dKOXqwfj=Jj^;+U7Q@; z-0hhsaT8xB6DpzaKcme8y`M!1U#2xDpzkuHHVhz*w9n{!$fypzZXBU zzavB6|3!7^eK7VLF!bGB)P@lReQy)#(Dy=7J0=h`JXD8t$gUY6@gS%Uxd638wqd^^ zLhVuAOpp^Ghy%4pb+bUwZ+B4p1Q1lGO!HqHs({3->MbX$4sQAfhan8I1C1Yz7uk+% zLH47*kbQ_B^^f{v0bvDU17Qc54RQyh8KeQE0^|nBd5~0)G>~);Q2FLB2)4M8ADQzf(iMDboX434;D^juwbE$O;hj_k_^j{y~4IM-c@5T`2T7 zoH#%@LAXHXfN+EGfbfE#zeS4vZs>dvJ`huoN{}j$YLFU`T#$<(c_5cSGC{IH(C@d= zZ>`bqqS0@dtw5|nY(Q*5>_7}b)`J*iKsJJyfouYyfcSu*-$0??E1};ONrFg$2!WvA+N0ma$AX~W+o0dZpx?2e z-<$-3NQ20LM1kxB2?YrQQ2>zxkq2o6@dUXAk^*uXdWp$9=aRFBF`Ak#snfuOiY94sKSKxTp^K0-w>Gqq$!Ug5m(h1Bwe22Uxqc@Od=| z8n-5h8i)pnI*1IdtOPllGg%M;5Hxpa9u+}UK~zA{dO-OKkmVqV#{fhhWEIFt5IqoG z5FHS05G{~3AnQOVAY>3P5Kj;f5O)v~h#iP6hz-bk5HwaJ5Ni-i5Hk={5Mz)HASNIi zLAHaSwX_)o#q?GX)E339Iqfr6hsx;lHkuBrM|dj`#OVg&3gQCd1hNCf0mL2z`4iPS zf}q@)_8GOs>d+jc*hT9J%>`Opq96ev{vbO+0zpt6k_eDs5JQkXAYmY(AR!=_zwq@~ z9r7dcAy!78!$J0f><8Hgg8Uu>5)BeXE2BEBEDxV!K@NhTxjszG(P!mE_#dR`+{v6- zb*rr6Hg9FPU@Csw|7E^kFwwYY!%(_~Uah~aa=M&}Zo7=6oFs$`A80s0lh9h@GWK+$ z8#Ho~3X(F?)U`JQG=VQ2ZbfpuSj0r{Vof2DVMy3FpcoEc3z3rkFu+8oASo*=2?31a zlo>Fmg@eVH?Iqa&n*#JBJGruv=H=Tee?j@($LJYR?7>)qgCU1?7;MBBbYLng*7`Wg6QeBc72j*XXx{Mp{xv z9*nC34H~IU8~>&`cD~C%BMak{R3x;4h8r}FOI(iluH-ue8rpzf(%9baFgz>}@%TFZ zw~>-u{K-xb{yNEzKlZA1?4?;Ct3Vh73>s-(+`ik=3j#%{8aXr`A{}*?30^Dxw)>)W zUM^^$UJh(Sf6D;bR@pJXLhfjs2^9nXrX)ltIvBaT5e{-ckj~mRpf-t70u}n57V?2m zS=%kXg6uKSz#Jf7JAnpy^vJyz4A&~Yo~N=YA~tKXzZ+V|+{MP{7v*$_P}yW9<)9zH zpxJIST$%ME_MtqD4KOb54)*R|u7tSUe2&0KSsSVb89;D%BS}!4TuJWrX5lxlUV2LH zhiY{&u(1If{He6fS|a6&)PB$;5Mn_y2Q*_ZC)_^dRMb=1sNO-pgMl~=CaKyk+0FW8 zAw3=XM>Ww(&82B}t!+znzaUpl?FYGyPzoC41ESu(7xt0X(V&r+R6&}5w)$^vQ$&9I z=YE{r97$eI6!PrEK_1?|76+=;(#UVe=_h@CV!S7Q?3Kz$DV2@7vg}E= zj@IC%EqCJ>BWLE>fku9k%@u7!kkSjPS_|7_^+5v>k1Y7-S@>rk*t3Rp zv$qS$3-TL^lA(u7cI}|rMvWa?cNcf0IoTr+vEkCRbD)9vK)$As{3sHRB)?_zcL^-; z*XKa}OtPJ0q+Z8^Z9EsNY0hL*28;~E0boi%GYkAyIxQugSf7OWzdEV z)yN0!6e~KR$}RM0PN^;+*t)wrJHc_`$i4J_8IxDjXd?x>_W)o43|rrc$1(@3e31r) zq8x!9CTBKi7ADa@kmo3XwE)Ws2;~W!pqUFAcAdy`jg_ff-`W0pl|dsFKtq-O>y?R% zP7JvbQayB!td25|W6*8XHbVUX1T-z6fvaHfm!tcS50!Kqe&6H&dM#n6djoBip{LLY4cdDLjH9b~c<;=bv;f&g51TbFX!2hj=RKMI9_`974QQwM`$$Ba zZvkM?NYfgQ`Jdi#*9kDn;8O6xFldmk4@qr_5h+wtq5n1u^6Zm7nBlHkGq@q~3haD} zk}@ii%7ik|p#4p$BINdpa6)d}ZMCsf?NYhFVqXgu;;wL(NBmRNpY zB@{BO2C#nR>7KKxJXb!#NM&I_;L#`03BsH&vwXEn?{o&$*VMiDEls0%qnxjcaSX+% z%;a+X3L2Q!;NgnRwO(GsW>hxnX2k}EQTLtYV&XxK>c^-U*yZ8wgkk5#%4v zyIBJod5B&`xK{%V^6010lg>sLQYPn=dZm@5v2C)ycd=(i2`9B5B^2=#GKu5?Y^U7V zEV#*)aGe9&sJp)kjV)a~nuS^0b}bb{jh%HgwiU?>iWfv)QlMg}8a6ngpxMTq>`)xY z{lo4rjRGMZ*wD&qOz__Dsv$I+#wM%$!|o3n+V1~1J^-81ERe_w5^&)pT)*7kLy+M> z*E!gC;M^y^7X@2CXdE5B_>B0nOy4gy3&YqBwzfHU?Twzq^hFoxIU_ zz}IBhtYL!*7HZpHk&?a>`5MgvSg>i{WE^yK8ah7a5DfzjZO!$92JMM`^o$Qmc5o(u z1|1Nt9~$3KD>r0_Bm*fZJ;5M-JI;c3?3|`3?^0U(bpXwb>JW+ap4Ta+N`OIoB6x@5?}2WL z2@X%UaOXMbq0>6ruVLB$$QDHPwT%3a7J$Lj^dp8gKl165~nJ1U>K2-%tyGM+G!+LIKP! z(4a^R{gjdag?Qi)k>7i zU$BAMKlX!-^zStgI)7s|=C>c`6e1o7|GuBeJC@)@lk4Yj*Lq8Za8o!1P*uOmwdA4}+4mm-rn^Cc!Zj@>@HQ=hCcMcfOU!PMb5p2Be?a@tB zwSjK*(twIsz<{f%d*Tmq4Xa)bX?~1r&`_^t3c{1~Bdz=Xb@A>Qa3Y5Jfqm@n)*O`$ zBL2tN!TR~XYuk?z|7#0=v@M8s^Zd6fi|Xqi`vDB~eEo0OghVEY_6eZF^a=WHJtXE(m&WxhxGaiakrSCzE_gZWMCy!BC#P zLy^5i>U|bWDdC^bV&{Pkop)Gw7xC^9^LP2qRzcJ3w$Bi%jQPOuUBkF^@-(P&K}Uz3 zJ{qE`723j*3;ySR{#o;H`uXurA_(0{z&ZZMJBc7!*nDLrmH*~W0vx9Z3-w2g82Ah& zWB4!|#^sz?a;~@L3Csef`De^OYwqVxvie<|%>Va2CYj{wL-LZ;^P`Zw+^k(pC|*u( z4(b9j-yhE^e}6pt!zxYD4^3Hnz#OdOuBPd;CUGCFxbGrn27qicpQg^Zs1!MmEzTp3>Q_r!r;3e~ao zrl}6`dzMnjWRe%yyX~Gph_U^ek0&9L(RFfU!q(PJ($?K|xvZ?Ll$Mtt3_-%1;^aaWf-5rIL}@}BDQbo0e zJ8KV^R*A{xSnVY0`#5m}^d;Gl;k`DABH`ig;t#L4T^uPC4>F9-i{#)0z4%MIdARN% zOM-u-Xit2lB(P5su!X>$BuQ}y(6qC*ag?%ivWEBIWQvn5S(Ey-3=I<=nvtEz5 zb_Ic!E+kr!Y;BJ|Vl^;!CwqS=+d4~7ysT|W5S$*+*anca-ch|989liAwWZ(N;%xqg zm!Lk7MaKVx31pMmQJxl(T z%diL90L4B^pkuoHF9tz-tPU{RCqQAbIl0hrllLp@{~D3lIR07>eW1I~pS#zU3~*O6 zv_+2md0Xlub7)S@Awbas%%3A@n-*?x0LPW6v#0PYlK)w(KrmSbEIP*h+yLBa1t6{@ zfwn%lo&Px;dsPEi>?1X3e}$tx^amL26QEGA|Gc+v`w-y1eWu3Oui}#Wf(lU79G0yt zxaQ}5{jenh8n;1Gd!^3qq_aI>yZcF8QeR5|12v~&e>GOxD-OWWK1~Vr-53CVFHpVv zt2Kzd1_31YaY}PZ?FQ?P>Rak;P^%?u-QD2&Pnj%C?o2;%6TaCu#z_>I0`CdXq)j$L z?~KUs4g_6{e4PRUTz=;M|7{4&|62jEj&8<)8=}hE%gfr|>HjouZOe_MF!WdE2iU~d z*l+n2`jWV3Y`4p_C&|=&{GKDAfw%*mecH;@7%;jZgph=64jDG1S-=(bfQY)4*Q~(* zxI`{S_NZhpEUXji2=4kKq7sICp8;s@2WRd48*Ss8QFuez*~4qE7v5sG;YID#Z`9T` z`8$xWRY1cJs5sp7(#doWUx0Bf;kC5x$ZY~-)T0~3dmNiqY1877=%?u}zqjg)*J-f- zr={;j&$GQ=Zs!l)@R*WOk7yQ1MLnz!#0u&Wr~x>1lzk^F0H=&oM9E4-QY8jPDFA$I zL$k~FIXJ0~b?cdfYI)h#vj&`P00xn)+_=lZC->jT|_s(*jcW`M87 zT~W7d2ZMum(KGz)%m#f41M**fH%oSR>;a==l(vBnkcAygRtxB)uk7n)0fl`p1*~ld z<0Ye%MjM2cV1-T}{kObD^T25U10?A{gv{QfQDD<089$4s4qp0@V^*FV^!k6z)ooNx9oe(&> zfbM``Mk_0+86@NBARKq09M0XfXQ&GDM zZo1l8j|`4@8j!?Ym|ngm7Y;$#IM0Jf8P|(*32+Z;F;PSB!Z16bI`$PSq2&7+f&ATE z(7PK$^fAMI4-60o^+=H$apL4G$fBD5)V|m4W$5}dJOv;8&q_K>Ob4kcEF!4VEq=$3 zmvzje7`&3ry59|;x*xQ1ENuN~F@R!hV?9s!%UjOGtN=Rc!(DJjA)~kcG$J8I<>v>_ zp0i~cKxe}caZAw&fKV+!cAIu+>w#IQ`uW`UXLU7*%5~!@HTsHdj(QIU(v9frDU2Pv z^X;9J)a&`?LBrtbW{!Ro*j7uKdNRLjKWgU+&U+Mgm`E}!oWev9h$%+Sp|JXI;=|%T zp~J|xu%)X9vUK2ExKjC!jBywK<0{<-CaAu6SS(yPaJ7l{q3@4I90Yj*P{->{)w({kh){RseCT~>42JIqMVI9E z8o$d(eVmivVt}<0pNZ^3+mVP+qqC4fC&e>BC7;4cDTYp?bAYA;&>inR$c_8@ae<_| zW0Uq&FFt`j`cK5gpJuSKUmz7*`M5a+1ykt&uHs!Wkvt_iV451n1?^l^WN^ZSjGU8s zz?$@Dc^$4O2e7V%-D1s@M1`%{styl8dcY-)w)r?gq0g*N#+%Hq3}fRQ3eC?4L*>>?=WekMhG7B;x@&^09}EBJ=R!$bxSTuK-$|<#8Y%o!WBxA z$a?^ce|e3HSY0|+m$SCRi&`#sK-ybgXqNUqv&tg7u}cwCosrvepjI~sYg^b+{sb*A zrYWxkD5i#Cl*-JcW`bp~r2x}!6gC!w-TuzjsL|5`?AYePDeJNoo7F@>k1B~=NqCB_-m>~?HWrvjIG+>l5I=0Q#>k2Qy;a>J?HXSQf~+Z@+sgm&SAg|NU@O6G1v_O zeZJwRV!C-Sa9mH?^N}QnGgfJGKBy${Vxx+ilkh(9w_-Hq)b+g{AA0OMM+3%ifggdtf`Ckn=Mf)_9yb%;Y;Lja%Kj->!8Ty=(II1#_ zn4|%%pj(&UC2wVTV*%3NT;%{V0*vpGa@~UUVn|DhSCOK1cezd7tLy`vE1LwgYXxCt zRe2x)6+j|aP4U>;H>+J4EWz~wn$Uq#qtajSFwR2tOw+#S!zCB=GilhFlaAtj zpEqbxkaccz`s0g1!HaL=STbc@*lklJ%JzHD*(+<-@Vjs3t?d(&TCe}v zK5iEKr`2YmSM{!Hn$3QrUzW{M@2h(CxZG5=Uhi7H^jJJiyc-$hroVw9vq4`VkCy1j zxut%G@tIPMr31QgHLl7E6ZTN^KsZv4j%PV_Qt$s+3}go^Ajvp}eiJMQ%E; zT=ClrK;tgC-WDT7M>=}s2b!n{UyEhHWFz9zKJn+ID#ulpV&}Sb*$zw)N(zgXGT^X} X<6bs639t0M5z&Y_u?4aJzyJRQdum`7 literal 0 HcmV?d00001 diff --git a/package.json b/package.json new file mode 100644 index 0000000..1f7d950 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "mail-server", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": { + "dev": "DEBUG='ndk:*' bun --watch src/index.ts", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev" + }, + "dependencies": { + "@arx/utils": "git+ssh://git@git.arx-ccn.com:222/Arx/ts-utils#v0.0.4", + "@elysiajs/cors": "^1.1.1", + "@elysiajs/server-timing": "^1.1.0", + "@elysiajs/swagger": "^1.1.6", + "@libsql/client": "^0.14.0", + "@nostr-dev-kit/ndk": "^2.10.6", + "@prisma/adapter-libsql": "^5.22.0", + "@prisma/client": "5.22.0", + "elysia": "^1.1.25", + "node-forge": "^1.3.1", + "smtp-server": "^3.13.0", + "websocket-polyfill": "^1.0.0", + "winston": "^3.17.0" + }, + "devDependencies": { + "@types/node-forge": "^1.3.9", + "@types/smtp-server": "^3.5.10", + "bun-types": "latest", + "prisma": "5.22.0", + "typescript": "^5.3.2" + }, + "private": true +} diff --git a/prisma/migrations/20241125122247_init/migration.sql b/prisma/migrations/20241125122247_init/migration.sql new file mode 100644 index 0000000..aa6b44d --- /dev/null +++ b/prisma/migrations/20241125122247_init/migration.sql @@ -0,0 +1,29 @@ +-- CreateTable +CREATE TABLE "users" ( + "npub" TEXT NOT NULL PRIMARY KEY, + "registeredAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastPayment" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "subscriptionDuration" INTEGER +); + +-- CreateTable +CREATE TABLE "aliases" ( + "npub" TEXT NOT NULL, + "alias" TEXT NOT NULL, + + PRIMARY KEY ("npub", "alias"), + CONSTRAINT "aliases_npub_fkey" FOREIGN KEY ("npub") REFERENCES "users" ("npub") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "mail_queue" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "alias" TEXT NOT NULL, + "sender" TEXT NOT NULL, + "data" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "mail_queue_alias_fkey" FOREIGN KEY ("alias") REFERENCES "aliases" ("alias") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "aliases_alias_key" ON "aliases"("alias"); diff --git a/prisma/migrations/20241126193747_remove_mail_queue/migration.sql b/prisma/migrations/20241126193747_remove_mail_queue/migration.sql new file mode 100644 index 0000000..a0e1043 --- /dev/null +++ b/prisma/migrations/20241126193747_remove_mail_queue/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the `mail_queue` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "mail_queue"; +PRAGMA foreign_keys=on; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..42ccefa --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,28 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "sqlite" + url = env("DB_URL") +} + +model User { + npub String @id + registeredAt DateTime @default(now()) + lastPayment DateTime @default(now()) + subscriptionDuration Int? + aliases Alias[] + + @@map("users") +} + +model Alias { + npub String + alias String @unique + user User @relation(fields: [npub], references: [npub]) + + @@id([npub, alias]) + @@map("aliases") +} diff --git a/src/httpServer.ts b/src/httpServer.ts new file mode 100644 index 0000000..e56b85e --- /dev/null +++ b/src/httpServer.ts @@ -0,0 +1,247 @@ +import {CashuMint, CashuWallet, getEncodedToken} from "@cashu/cashu-ts"; +import {logger} from "./utils"; +import * as nip98 from "nostr-tools/nip98"; +import {Elysia, t} from "elysia"; +import {swagger} from "@elysiajs/swagger"; +import {serverTiming} from "@elysiajs/server-timing"; +import {PrismaClient} from "@prisma/client"; +import {TokenInfoWithMailSubscriptionDuration} from "@arx/utils/cashu.ts"; +import {npubToPubKeyString, pubKeyStringToNpub} from "@arx/utils/nostr.ts"; +import cors from "@elysiajs/cors"; + +const npubType = t.String({ + pattern: `^npub1[023456789acdefghjklmnpqrstuvwxyz]{58}$`, + error: 'Invalid npub format' +}); + +const cashuTokenType = t.String({ + pattern: '^cashu[A-Za-z0-9+-_]*={0,3}$', + error: 'Invalid Cashu token format' +}) + +export class HttpServer { + constructor(private db: PrismaClient, port: number) { + new Elysia() + .use(swagger({ + documentation: { + info: { + title: 'npub.email Documentation', + version: '0.0.1' + } + } + })) + .use(serverTiming()) + .use(cors()) + .get('/', 'nostr.email server') + .get('/subscription/:npub', this.getSubscriptionForNpub, { + params: t.Object({ + npub: npubType + }) + }) + .get('/aliases/:npub', this.getAliasesForNpub, { + params: t.Object({ + npub: npubType, + }), + }) + .get('/alias/:alias', this.getNpubForAlias, { + params: t.Object({ + alias: t.String(), + }), + }) + .post('/addAlias', this.addAlias, { + body: t.Object({ + alias: t.String() + }) + }) + .post('/addTime/:npub', this.addTimeToNpub, { + params: t.Object({ + npub: npubType, + }), + body: t.Object({ + tokenString: cashuTokenType + }) + }) + .listen(port) + logger.info(`HTTP Server running on port ${port}`); + } + + getSubscriptionForNpub = async ({params: {npub}}: { + params: { + npub: string + } + }) => { + const user = await this.db.user.findFirst({ + where: { + npub + }, + include: { + aliases: true + } + }); + if (!user) return { + subscribed: false + }; + return { + subscribed: true, + subscribedUntil: user.subscriptionDuration == null ? Infinity : Math.floor(user.lastPayment.getTime() / 1000) + user.subscriptionDuration + }; + } + + getNpubForAlias = async ({params: {alias}}: { + params: { + alias: string + } + }) => { + const user = await this.db.user.findFirst({ + where: { + aliases: { + some: { + alias + } + } + } + }); + if (!user) return new Response('Not found', { + status: 404 + }); + return user.npub; + } + + getAliasesForNpub = async ({params: {npub}, headers}: { + params: { + npub: string + }, + headers: Record + }) => { + const unpacked = await this.getUnpackedAuthHeader(headers, `/aliases/${npub}`); + const npubAsPubkey = npubToPubKeyString(npub); + if (unpacked.pubkey !== npubAsPubkey) + return new Response('Unauthorized', { + status: 401 + }) + const user = await this.db.user.findFirst({ + where: { + npub + }, + include: { + aliases: true + } + }); + if (!user) return new Response('Not found', { + status: 404 + }); + return user.aliases.map(alias => alias.alias); + } + + addAlias = async ({body: {alias}, headers}: { + body: { + alias: string + }, + headers: Record + }) => { + const unpacked = await this.getUnpackedAuthHeader(headers, '/addAlias'); + const unpackedKeyToNpub = pubKeyStringToNpub(unpacked.pubkey); + const userInDb = await this.db.user.findFirst({ + where: { + npub: unpackedKeyToNpub + } + }); + if (!userInDb) return new Response('Unauthorized', { + status: 401 + }); + + const stillHasSubscription = userInDb.subscriptionDuration === null || Math.floor(userInDb.lastPayment.getTime() / 1000) + userInDb.subscriptionDuration > Date.now() / 1000; + if (!stillHasSubscription) return new Response('User has no subscription', { + status: 400 + }); + const aliasInDb = await this.db.alias.findFirst({ + where: { + alias + } + }); + if (aliasInDb) return new Response('Alias already exists', { + status: 400 + }); + return this.db.user.update({ + where: { + npub: unpackedKeyToNpub + }, + data: { + aliases: { + create: { + alias + } + } + } + }); + } + + addTimeToNpub = async ({params: {npub}, body: {tokenString}}: { + params: { + npub: string + }, + body: { + tokenString: string + } + }) => { + const userInDb = await this.db.user.findFirst({ + where: { + npub + } + }); + + if (userInDb && (userInDb.subscriptionDuration === null || userInDb.subscriptionDuration === -1)) + return new Response('User has unlimited subscription', { + status: 400 + }) + + const tokenInfo = new TokenInfoWithMailSubscriptionDuration(tokenString); + const mint = new CashuMint(tokenInfo.mint); + const wallet = new CashuWallet(mint); + const newToken = await wallet.receive(tokenString); + const encodedToken = getEncodedToken({ + token: [{ + mint: tokenInfo.mint, + proofs: newToken + }] + }); + logger.info(`New cashu token: ${encodedToken}`); + if (userInDb) { + let timeRemaining = Math.max(0, Math.floor((+new Date(userInDb.lastPayment.getTime() + userInDb.subscriptionDuration! * 1000) - +new Date()) / 1000)); + timeRemaining += tokenInfo.duration; + await this.db.user.update({ + where: { + npub + }, + data: { + lastPayment: new Date(), + subscriptionDuration: timeRemaining + } + }); + return { + newTimeRemaining: timeRemaining + } + } + await this.db.user.create({ + data: { + npub, + registeredAt: new Date(), + lastPayment: new Date(), + subscriptionDuration: tokenInfo.duration + } + }); + return { + newTimeRemaining: tokenInfo.duration + } + } + + private getUnpackedAuthHeader = async (headers: Record, url: string) => { + if (!headers.authorization) + throw new Error('Unauthorized'); + const authHeader = headers.authorization.split(' ')[1]; + const validate = await nip98.validateToken(authHeader, `${process.env.PUBLIC_API_BASE_URL!}${url}`, "POST"); + if (!validate) + throw new Error('Unauthorized'); + return await nip98.unpackEventFromToken(authHeader); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7d96c8f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,24 @@ +import {createClient as createLibSQLClient} from "@libsql/client"; +import "websocket-polyfill"; +import {PrismaClient} from "@prisma/client"; +import {PrismaLibSQL} from "@prisma/adapter-libsql"; +import {NostrSmtpServer} from "./smtpServer"; +import {HttpServer} from "./httpServer"; + +if (!process.env.BASE_DOMAIN) + throw new Error("BASE_DOMAIN is not set"); +if (!process.env.DB_URL) + throw new Error("DB_URL is not set"); +if (!process.env.PUBLIC_API_BASE_URL) + throw new Error("PUBLIC_API_BASE_URL is not set"); + +const dbClient = createLibSQLClient({ + url: process.env.DB_URL, +}); + +const db = new PrismaClient({ + adapter: new PrismaLibSQL(dbClient) +}); + +new NostrSmtpServer(db, parseInt(process.env.SMTP_PORT || '6587')); +new HttpServer(db, parseInt(process.env.HTTP_PORT || '3000')); \ No newline at end of file diff --git a/src/smtpServer.ts b/src/smtpServer.ts new file mode 100644 index 0000000..ced5372 --- /dev/null +++ b/src/smtpServer.ts @@ -0,0 +1,100 @@ +import {SMTPServer} from "smtp-server"; +import {getNDK} from "./utils"; +import {generateSecretKey} from "nostr-tools"; +import {NDKEvent, NDKKind, NDKPrivateKeySigner} from "@nostr-dev-kit/ndk"; +import {PrismaClient} from "@prisma/client"; +import {logger} from "./utils/logs"; +import {encryptEventForRecipient, parseEmail} from "@arx/utils"; + +export class NostrSmtpServer { + private server: SMTPServer; + + constructor(db: PrismaClient, port: number) { + this.server = new SMTPServer({ + authOptional: true, + logger: false, + + onData: (stream, session, callback) => { + let mailData = ''; + + stream.on('data', (chunk: Buffer) => { + mailData += chunk.toString(); + }); + + stream.on('end', async () => { + if (!session.envelope.mailFrom) { + logger.warn('Ignoring email without sender'); + callback(); + return; + } + try { + const parsedEmail = parseEmail(mailData); + for (let recipientEmail of session.envelope.rcptTo) { + const address = recipientEmail.address; + const parts = address.split('@'); + if (parts[1] !== process.env.BASE_DOMAIN) { + logger.warn('Not sending email to', address, 'because it is not in the allowed domain'); + continue; + } + const alias = parts[0]; + const user = await db.alias.findUnique({ + where: { + alias + }, + include: { + user: true + } + }); + if (!user) { + logger.warn('No user found for', alias, 'skipping'); + continue; + } + const timeRemainingInSubscription = user.user.subscriptionDuration === null ? Infinity : (user.user.subscriptionDuration * 1000) - Date.now() + user.user.lastPayment.getTime(); + if (timeRemainingInSubscription <= 0) { + logger.warn(`Subscription has expired for ${alias}`); + continue; + } + const recipient = user.npub; + const randomKey = generateSecretKey(); + const randomKeySinger = new NDKPrivateKeySigner(randomKey); + const ndk = getNDK(); + ndk.signer = randomKeySinger; + await ndk.connect(); + const ndkUser = ndk.getUser({ + npub: recipient + }); + const randomKeyUser = await randomKeySinger.user(); + const event = new NDKEvent(); + event.kind = NDKKind.Article; + event.content = parsedEmail.body; + event.created_at = Math.floor(Date.now() / 1000); + event.pubkey = randomKeyUser.pubkey; + event.tags.push(['p', ndkUser.pubkey]) + event.tags.push(['subject', parsedEmail.subject]); + event.tags.push(['email:localIP', session.localAddress]); + event.tags.push(['email:remoteIP', session.remoteAddress]); + event.tags.push(['email:isEmail', 'true']); + for (let to of session.envelope.rcptTo) + event.tags.push(['email:to', to.address]); + for (let header of Object.keys(parsedEmail.headers)) + event.tags.push([`email:header:${header}`, parsedEmail.headers[header]]); + event.tags.push(['email:session', session.id]); + event.tags.push(['email:from', session.envelope.mailFrom?.address ?? '']); + + await event.sign(randomKeySinger); + const encryptedEvent = await encryptEventForRecipient(ndk, event, ndkUser); + await encryptedEvent.publish(); + } + } catch (e) { + logger.error(JSON.stringify(e)); + } finally { + callback(); + } + }); + } + }); + + this.server.listen(port, '0.0.0.0'); + logger.info(`SMTP Server running on port ${port}`); + } +} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..b220d99 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,16 @@ +import NDK from "@nostr-dev-kit/ndk"; + +export * from "./logs"; + +export function getNDK() { + return new NDK({ + explicitRelayUrls: [ + 'wss://relay.primal.net', + 'wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://offchain.pub' + ], + autoConnectUserRelays: false, + enableOutboxModel: true, + }); +} diff --git a/src/utils/logs.ts b/src/utils/logs.ts new file mode 100644 index 0000000..80c0465 --- /dev/null +++ b/src/utils/logs.ts @@ -0,0 +1,23 @@ +import winston from "winston"; + +const {combine, timestamp, printf, align, colorize, json} = winston.format; + +export const logger = winston.createLogger({ + level: 'info', + transports: [ + new winston.transports.Console({ + format: combine( + colorize({all: true}), + timestamp({ + format: 'YYYY-MM-DD hh:mm:ss.SSS A', + }), + align(), + printf((info) => `[${info.timestamp}] ${info.level}: ${info.message}`) + ), + }), + new winston.transports.File({ + filename: process.env.LOG_FILE || '/tmp/nostr-email.log', + format: combine(timestamp(), json()), + }), + ], +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..83c5747 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "preserve", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "allowImportingTsExtensions": true + }, + "include": [ + "src" + ] +}