1
+ import base64
1
2
import smtplib
2
3
from datetime import datetime
3
4
from email .mime .image import MIMEImage
6
7
from email .utils import formatdate
7
8
from email .utils import make_msgid
8
9
10
+ import sendgrid # type: ignore
11
+ from sendgrid .helpers .mail import Attachment # type: ignore
12
+ from sendgrid .helpers .mail import Content
13
+ from sendgrid .helpers .mail import ContentId
14
+ from sendgrid .helpers .mail import Disposition
15
+ from sendgrid .helpers .mail import Email
16
+ from sendgrid .helpers .mail import FileContent
17
+ from sendgrid .helpers .mail import FileName
18
+ from sendgrid .helpers .mail import FileType
19
+ from sendgrid .helpers .mail import Mail
20
+ from sendgrid .helpers .mail import To
21
+
9
22
from onyx .configs .app_configs import EMAIL_CONFIGURED
10
23
from onyx .configs .app_configs import EMAIL_FROM
24
+ from onyx .configs .app_configs import SENDGRID_API_KEY
11
25
from onyx .configs .app_configs import SMTP_PASS
12
26
from onyx .configs .app_configs import SMTP_PORT
13
27
from onyx .configs .app_configs import SMTP_SERVER
18
32
from onyx .configs .constants import ONYX_SLACK_URL
19
33
from onyx .db .models import User
20
34
from onyx .server .runtime .onyx_runtime import OnyxRuntime
21
- from onyx .utils .file import FileWithMimeType
35
+ from onyx .utils .logger import setup_logger
22
36
from onyx .utils .url import add_url_params
23
37
from onyx .utils .variable_functionality import fetch_versioned_implementation
24
38
from shared_configs .configs import MULTI_TENANT
25
39
40
+ logger = setup_logger ()
26
41
27
42
HTML_EMAIL_TEMPLATE = """\
28
43
<!DOCTYPE html>
@@ -176,6 +191,70 @@ def send_email(
176
191
if not EMAIL_CONFIGURED :
177
192
raise ValueError ("Email is not configured." )
178
193
194
+ if SENDGRID_API_KEY :
195
+ send_email_with_sendgrid (
196
+ user_email , subject , html_body , text_body , mail_from , inline_png
197
+ )
198
+ return
199
+
200
+ send_email_with_smtplib (
201
+ user_email , subject , html_body , text_body , mail_from , inline_png
202
+ )
203
+
204
+
205
+ def send_email_with_sendgrid (
206
+ user_email : str ,
207
+ subject : str ,
208
+ html_body : str ,
209
+ text_body : str ,
210
+ mail_from : str = EMAIL_FROM ,
211
+ inline_png : tuple [str , bytes ] | None = None ,
212
+ ) -> None :
213
+ from_email = Email (mail_from ) if mail_from else Email ("noreply@onyx.app" )
214
+ to_email = To (user_email )
215
+
216
+ mail = Mail (
217
+ from_email = from_email ,
218
+ to_emails = to_email ,
219
+ subject = subject ,
220
+ plain_text_content = Content ("text/plain" , text_body ),
221
+ )
222
+
223
+ # Add HTML content
224
+ mail .add_content (Content ("text/html" , html_body ))
225
+
226
+ if inline_png :
227
+ image_name , image_data = inline_png
228
+
229
+ # Create attachment
230
+ encoded_image = base64 .b64encode (image_data ).decode ()
231
+ attachment = Attachment ()
232
+ attachment .file_content = FileContent (encoded_image )
233
+ attachment .file_name = FileName (image_name )
234
+ attachment .file_type = FileType ("image/png" )
235
+ attachment .disposition = Disposition ("inline" )
236
+ attachment .content_id = ContentId (image_name )
237
+
238
+ mail .add_attachment (attachment )
239
+
240
+ # Get a JSON-ready representation of the Mail object
241
+ mail_json = mail .get ()
242
+
243
+ sg = sendgrid .SendGridAPIClient (api_key = SENDGRID_API_KEY )
244
+ response = sg .client .mail .send .post (request_body = mail_json ) # can raise
245
+ if response .status_code != 202 :
246
+ logger .warning (f"Unexpected status code { response .status_code } " )
247
+
248
+
249
+ def send_email_with_smtplib (
250
+ user_email : str ,
251
+ subject : str ,
252
+ html_body : str ,
253
+ text_body : str ,
254
+ mail_from : str = EMAIL_FROM ,
255
+ inline_png : tuple [str , bytes ] | None = None ,
256
+ ) -> None :
257
+
179
258
# Create a multipart/alternative message - this indicates these are alternative versions of the same content
180
259
msg = MIMEMultipart ("alternative" )
181
260
msg ["Subject" ] = subject
@@ -210,13 +289,10 @@ def send_email(
210
289
html_part = MIMEText (html_body , "html" )
211
290
msg .attach (html_part )
212
291
213
- try :
214
- with smtplib .SMTP (SMTP_SERVER , SMTP_PORT ) as s :
215
- s .starttls ()
216
- s .login (SMTP_USER , SMTP_PASS )
217
- s .send_message (msg )
218
- except Exception as e :
219
- raise e
292
+ with smtplib .SMTP (SMTP_SERVER , SMTP_PORT ) as s :
293
+ s .starttls ()
294
+ s .login (SMTP_USER , SMTP_PASS )
295
+ s .send_message (msg )
220
296
221
297
222
298
def send_subscription_cancellation_email (user_email : str ) -> None :
@@ -264,27 +340,13 @@ def send_subscription_cancellation_email(user_email: str) -> None:
264
340
)
265
341
266
342
267
- def send_user_email_invite (
268
- user_email : str , current_user : User , auth_type : AuthType
269
- ) -> None :
270
- onyx_file : FileWithMimeType | None = None
271
-
272
- try :
273
- load_runtime_settings_fn = fetch_versioned_implementation (
274
- "onyx.server.enterprise_settings.store" , "load_runtime_settings"
275
- )
276
- settings = load_runtime_settings_fn ()
277
- application_name = settings .application_name
278
- except ModuleNotFoundError :
279
- application_name = ONYX_DEFAULT_APPLICATION_NAME
280
-
281
- onyx_file = OnyxRuntime .get_emailable_logo ()
282
-
283
- subject = f"Invitation to Join { application_name } Organization"
343
+ def build_user_email_invite (
344
+ from_email : str , to_email : str , application_name : str , auth_type : AuthType
345
+ ) -> tuple [str , str ]:
284
346
heading = "You've Been Invited!"
285
347
286
348
# the exact action taken by the user, and thus the message, depends on the auth type
287
- message = f"<p>You have been invited by { current_user . email } to join an organization on { application_name } .</p>"
349
+ message = f"<p>You have been invited by { from_email } to join an organization on { application_name } .</p>"
288
350
if auth_type == AuthType .CLOUD :
289
351
message += (
290
352
"<p>To join the organization, please click the button below to set a password "
@@ -309,7 +371,7 @@ def send_user_email_invite(
309
371
raise ValueError (f"Invalid auth type: { auth_type } " )
310
372
311
373
cta_text = "Join Organization"
312
- cta_link = f"{ WEB_DOMAIN } /auth/signup?email={ user_email } "
374
+ cta_link = f"{ WEB_DOMAIN } /auth/signup?email={ to_email } "
313
375
314
376
html_content = build_html_email (
315
377
application_name ,
@@ -322,13 +384,36 @@ def send_user_email_invite(
322
384
# text content is the fallback for clients that don't support HTML
323
385
# not as critical, so not having special cases for each auth type
324
386
text_content = (
325
- f"You have been invited by { current_user . email } to join an organization on { application_name } .\n "
387
+ f"You have been invited by { from_email } to join an organization on { application_name } .\n "
326
388
"To join the organization, please visit the following link:\n "
327
- f"{ WEB_DOMAIN } /auth/signup?email={ user_email } \n "
389
+ f"{ WEB_DOMAIN } /auth/signup?email={ to_email } \n "
328
390
)
329
391
if auth_type == AuthType .CLOUD :
330
392
text_content += "You'll be asked to set a password or login with Google to complete your registration."
331
393
394
+ return text_content , html_content
395
+
396
+
397
+ def send_user_email_invite (
398
+ user_email : str , current_user : User , auth_type : AuthType
399
+ ) -> None :
400
+ try :
401
+ load_runtime_settings_fn = fetch_versioned_implementation (
402
+ "onyx.server.enterprise_settings.store" , "load_runtime_settings"
403
+ )
404
+ settings = load_runtime_settings_fn ()
405
+ application_name = settings .application_name
406
+ except ModuleNotFoundError :
407
+ application_name = ONYX_DEFAULT_APPLICATION_NAME
408
+
409
+ onyx_file = OnyxRuntime .get_emailable_logo ()
410
+
411
+ subject = f"Invitation to Join { application_name } Organization"
412
+
413
+ text_content , html_content = build_user_email_invite (
414
+ current_user .email , user_email , application_name , auth_type
415
+ )
416
+
332
417
send_email (
333
418
user_email ,
334
419
subject ,
0 commit comments