1- use chrono:: { DateTime , Utc } ;
21use hmac:: { Hmac , Mac } ;
3- use lettre:: {
4- transport:: smtp:: authentication:: Credentials , AsyncSmtpTransport , AsyncTransport , Message ,
5- Tokio1Executor ,
6- } ;
2+ use lettre:: AsyncTransport ;
73use sha2:: Sha256 ;
84use std:: collections:: HashMap ;
9- use std:: fs:: OpenOptions ;
10- use std:: io:: Write ;
115use std:: sync:: Arc ;
126use tokio:: fs:: File ;
137use tokio:: io:: { AsyncBufReadExt , BufReader } ;
8+
9+ use chrono:: { DateTime , Utc } ;
10+ use tokio:: fs:: OpenOptions ;
11+ use tokio:: io:: AsyncWriteExt ;
1412use tokio:: sync:: RwLock ;
1513
1614use super :: config:: SmtpConfig ;
1715
1816type HmacSha256 = Hmac < Sha256 > ;
1917
18+ use lettre:: {
19+ transport:: smtp:: authentication:: Credentials , AsyncSmtpTransport , Message , Tokio1Executor ,
20+ } ;
21+
2022#[ derive( Clone ) ]
2123pub struct EmailStore {
2224 pub store : Arc < RwLock < HashMap < String , DateTime < Utc > > > > ,
2325 file : String ,
2426 smtp : SmtpConfig ,
2527 secret : Vec < u8 > ,
2628 pub web_host : String ,
29+
30+ mailer : Arc < AsyncSmtpTransport < Tokio1Executor > > ,
2731}
2832
2933impl EmailStore {
3034 pub fn new ( file : String , smtp : SmtpConfig , secret : Vec < u8 > , web_host : String ) -> Self {
35+ let mailer = AsyncSmtpTransport :: < Tokio1Executor > :: relay ( & smtp. server )
36+ . expect ( "invalid smtp server" )
37+ . credentials ( Credentials :: new (
38+ smtp. username . clone ( ) ,
39+ smtp. password . clone ( ) ,
40+ ) )
41+ . build ( ) ;
42+
3143 Self {
3244 store : Arc :: new ( RwLock :: new ( HashMap :: new ( ) ) ) ,
3345 file,
3446 smtp,
3547 secret,
3648 web_host,
49+ mailer : Arc :: new ( mailer) ,
3750 }
3851 }
3952
@@ -43,31 +56,10 @@ impl EmailStore {
4356 hex:: encode ( mac. finalize ( ) . into_bytes ( ) )
4457 }
4558
46- pub async fn load_trials ( & self ) -> std:: io:: Result < ( ) > {
47- let file = match File :: open ( & self . file ) . await {
48- Ok ( f) => f,
49- Err ( _) => return Ok ( ( ) ) ,
50- } ;
51-
52- let reader = BufReader :: new ( file) ;
53- let mut lines = reader. lines ( ) ;
54-
55- let mut map = HashMap :: new ( ) ;
56-
57- while let Some ( line) = lines. next_line ( ) . await ? {
58- let parts: Vec < & str > = line. split ( ',' ) . collect ( ) ;
59-
60- if parts. len ( ) >= 2 {
61- if let Ok ( ts) = parts[ 0 ] . parse :: < DateTime < Utc > > ( ) {
62- map. insert ( parts[ 1 ] . to_string ( ) , ts) ;
63- }
64- }
65- }
66-
67- let mut store = self . store . write ( ) . await ;
68- * store = map;
69-
70- Ok ( ( ) )
59+ pub async fn check_email_hmac ( & self , email : & str ) -> bool {
60+ let email_hmac = self . hmac_email ( email) ;
61+ let store = self . store . read ( ) . await ;
62+ store. contains_key ( & email_hmac)
7163 }
7264
7365 pub async fn save_trial_hmac (
@@ -84,148 +76,90 @@ impl EmailStore {
8476 store. insert ( email_hmac. clone ( ) , * time) ;
8577 }
8678
87- let mut file = OpenOptions :: new ( )
79+ let file = OpenOptions :: new ( )
8880 . create ( true )
8981 . append ( true )
90- . open ( & self . file ) ?;
82+ . open ( & self . file )
83+ . await ;
9184
92- writeln ! (
93- file,
94- "{},{},{},{}" ,
85+ let line = format ! (
86+ "{},{},{},{}\n " ,
9587 time. to_rfc3339( ) ,
9688 email_hmac,
9789 sub_id,
9890 ref_by
99- ) ?;
91+ ) ;
92+
93+ file?. write_all ( line. as_bytes ( ) ) . await ?;
10094
10195 Ok ( ( ) )
10296 }
10397
104- pub async fn check_email_hmac ( & self , email : & str ) -> bool {
105- let email_hmac = self . hmac_email ( email) ;
98+ pub async fn load_trials ( & self ) -> std:: io:: Result < ( ) > {
99+ let file = match File :: open ( & self . file ) . await {
100+ Ok ( f) => f,
101+ Err ( _) => return Ok ( ( ) ) ,
102+ } ;
106103
107- let store = self . store . read ( ) . await ;
108- store. contains_key ( & email_hmac)
109- }
104+ let reader = BufReader :: new ( file) ;
105+ let mut lines = reader. lines ( ) ;
110106
111- pub async fn send_email (
112- & self ,
113- to : & str ,
114- sub_id : & uuid:: Uuid ,
115- ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
116- let html_body = format ! (
117- r#"
118- <!DOCTYPE html>
119- <html>
120- <head>
121- <meta charset="UTF-8">
122- <title>FRKN Рилзопровод</title>
123- <style>
124- body {{
125- font-family: Arial, sans-serif;
126- background-color: #f4f4f4;
127- margin: 0;
128- padding: 0;
129- }}
130- .container {{
131- width: 100%;
132- max-width: 600px;
133- margin: 0 auto;
134- background-color: #ffffff;
135- padding: 20px;
136- border-radius: 12px;
137- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
138- }}
139- .header {{
140- text-align: center;
141- margin-bottom: 20px;
142- }}
143- .logo {{
144- max-width: 150px;
145- }}
146- h1 {{
147- color: #1d4ed8;
148- font-size: 24px;
149- }}
150- p {{
151- color: #374151;
152- font-size: 16px;
153- line-height: 1.5;
154- }}
155- .button {{
156- display: inline-block;
157- padding: 12px 24px;
158- background-color: #1d4ed8;
159- color: #ffffff;
160- text-decoration: none;
161- border-radius: 8px;
162- margin: 20px 0;
163- font-weight: bold;
164- }}
165- .footer {{
166- font-size: 12px;
167- color: #9ca3af;
168- text-align: center;
169- margin-top: 20px;
170- }}
171- </style>
172- </head>
173- <body>
174- <div class="container">
175- <div class="header">
176- <h1>Твоя подписка FRKN активирована!</h1>
177- </div>
178- <p>Привет!</p>
179- <p>Твоя подписка для <strong>FRKN</strong> успешно активирована 🎉</p>
180-
181- <a href="{web_host}/subscription?id={sub_id}"
182-
183- style="
184- display: inline-block;
185- padding: 12px 24px;
186- background-color: #1d4ed8;
187- color: #ffffff !important;
188- text-decoration: none;
189- border-radius: 8px;
190- font-weight: bold;">Перейти к подписке</a>
191-
192- <br><br>
193- <p>
194- Твой <strong>ID:</strong> {sub_id}<br/>
195- <div style="font-size: small;">ID это твой уникальный идентификатор подписки, рекомендуем его записать или запомнить</div>
196- <div style="font-size: small;">Мы не храним твой email</div>
197-
198- </p>
199-
200- <p>Подписывайся на наш Telegram: <a href="https://t.me/frkn_org">@frkn_org</a></p>
201- <br>
202- <div class="footer">
203- <a href="https://t.me/frkn_support">Поддержка</a></p> • Vive la résistance!<br/>
204- {web_host} • © 2026 FRKN Privacy Company
205- </div>
206- </div>
207- </body>
208- </html>
209- "# ,
210- sub_id = sub_id,
211- web_host = self . web_host,
212- ) ;
107+ let mut map = HashMap :: new ( ) ;
213108
214- let msg = Message :: builder ( )
215- . from ( format ! ( "FRKN <{}>" , self . smtp. from) . parse ( ) ?)
216- . to ( to. parse ( ) ?)
217- . subject ( "FRKN Рилзопровод" )
218- . header ( lettre:: message:: header:: ContentType :: TEXT_HTML )
219- . body ( html_body) ?;
109+ while let Some ( line) = lines. next_line ( ) . await ? {
110+ let parts: Vec < & str > = line. split ( ',' ) . collect ( ) ;
220111
221- let mailer = AsyncSmtpTransport :: < Tokio1Executor > :: relay ( & self . smtp . server ) ?
222- . credentials ( Credentials :: new (
223- self . smtp . username . clone ( ) ,
224- self . smtp . password . clone ( ) ,
225- ) )
226- . build ( ) ;
112+ if parts. len ( ) >= 2 {
113+ if let Ok ( ts) = parts[ 0 ] . parse :: < DateTime < Utc > > ( ) {
114+ map. insert ( parts[ 1 ] . to_string ( ) , ts) ;
115+ }
116+ }
117+ }
118+
119+ let mut store = self . store . write ( ) . await ;
120+ * store = map;
227121
228- mailer. send ( msg) . await ?;
229122 Ok ( ( ) )
230123 }
124+
125+ pub async fn send_email_background ( & self , to : String , sub_id : uuid:: Uuid ) {
126+ let mailer = self . mailer . clone ( ) ;
127+ let web_host = self . web_host . clone ( ) ;
128+ let from = self . smtp . from . clone ( ) ;
129+ tokio:: spawn ( async move {
130+ let html_body = format ! (
131+ r#"
132+ <html>
133+ <body>
134+ <h2>Твой Тест-Драйв активирован</h2>
135+ <p>Ссылка:</p>
136+ <a href="{web_host}/subscription?id={sub_id}">
137+ Открыть подписку
138+ </a>
139+ </body>
140+ </html>
141+ "# ,
142+ web_host = web_host,
143+ sub_id = sub_id,
144+ ) ;
145+
146+ let msg = match Message :: builder ( )
147+ . from ( from. parse ( ) . unwrap ( ) )
148+ . to ( to. parse ( ) . unwrap ( ) )
149+ . subject ( "Тест-Драйв" )
150+ . header ( lettre:: message:: header:: ContentType :: TEXT_HTML )
151+ . body ( html_body)
152+ {
153+ Ok ( m) => m,
154+ Err ( e) => {
155+ tracing:: error!( "Email build error: {}" , e) ;
156+ return ;
157+ }
158+ } ;
159+
160+ if let Err ( e) = mailer. send ( msg) . await {
161+ tracing:: error!( "Email send error: {}" , e) ;
162+ }
163+ } ) ;
164+ }
231165}
0 commit comments