-
Notifications
You must be signed in to change notification settings - Fork 15
43717 backend [ mail ] Create a switchable SMTP email client. #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
43717 backend [ mail ] Create a switchable SMTP email client. #82
Conversation
…Customer.io support - Refactored email sending logic to support multiple email clients. - Introduced EmailTemplateModel for managing email templates. - Updated settings to configure email backend based on client type. - Created EmailClient interface and specific implementations for SMTP and Customer.io. - Enhanced EmailService to utilize the new email client factory for sending emails. - Updated admin interface for managing email templates.
…fications - Updated EmailTemplateModel to support multiple template types using ArrayField. - Refactored admin interface for managing email templates, including new fields for name and template types. - Improved SMTPEmailClient to utilize the new template types for email sending. - Added new HTML templates for various notification types (auth, digest, task). - Updated settings for email configuration with default values for EMAIL_PORT and EMAIL_TIMEOUT.
- Introduced EmailClientProvider enum to manage email client types. - Updated settings to use EMAIL_PROVIDER for selecting email backend. - Refactored email client retrieval logic to simplify client instantiation. - Removed deprecated EmailClientFactory and streamlined email client management.
…module - Updated import paths for EmailService across various modules to reflect its new location in the notifications service. - Removed the deprecated email.py and tasks.py files from the services directory. - Introduced a new utility function for email client retrieval in the notifications module.
…_create_switchable_SMTP_email_client
- Renamed EmailClientProvider to EmailProvider for consistency. - Introduced EmailType enum to manage email types across the application. - Refactored EmailTemplateModel to use email_types instead of template_types. - Updated email sending logic in EmailService to utilize the new EmailType enum. - Removed deprecated email templates and added new HTML templates for task and digest notifications. - Enhanced admin forms for email templates to support multiple email types selection.
… enhance test coverage
…l content and layout improvements
…_create_switchable_SMTP_email_client
…_create_switchable_SMTP_email_client
…matting and content clarity
| data={ | ||
| 'title': 'Forgot Your Password?', | ||
| 'content': ( | ||
| '<h2><strong>We got a request to reset your ' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove any html markup from the code. Markup is only in the template; create "sub_title" variable if need.
sub_title = 'We got a request to reset your Pneumatic account password.'
content = 'A strong password includes eight or more '
'characters and a combination of uppercase and '
'lowercase letters, numbers and symbols, and is not '
'based on words in the dictionary.Add more variables as "top_content", "middle_content", "bottom content", "title", "sub_title" or any other for more flexible template customization
…ased notification system for user actions
…date related email templates and services
…workflow starter details in notifications
… improved clarity and user engagement
…th task status links and improved digest structure
…ification messages for improved clarity
| template_string: str, | ||
| context: Dict[str, Any], | ||
| ) -> str: | ||
| template = Template(template_string) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_render_template_string doesn’t handle template errors. A bad DB template (e.g., syntax error) will raise and abort send_email, potentially leaving the SMTP connection open. Consider catching exceptions here and falling back to the raw string (or another safe default) so delivery continues or fails in a controlled way.
| template = Template(template_string) | |
| try: | |
| template = Template(template_string) | |
| return template.render(Context(context)) | |
| except Exception: | |
| return template_string |
🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
Add switchable SMTP email client and route notifications through
src.notifications.services.email.EmailService._dispatch_emailwith provider selection viasettings.EMAIL_PROVIDERIntroduce a pluggable email client layer with
CustomerIOEmailClientandSMTPEmailClient, centralize send routing inEmailService._dispatch_email, add account-scopedEmailTemplateModel, and add HTML templates for task, digest, and auth notifications. Replace direct Customer.io usage withget_email_client()and update imports, tests, and settings to select provider at runtime.📍Where to Start
Start with provider selection in
get_email_clientin backend/src/notifications/clients/utils.py, then review dispatch logic inEmailService._dispatch_emailin backend/src/notifications/services/email.py.Changes since #82 opened
EmailService._send_email_to_consolemethod to properly construct message_vars string [81d6de7]EmailServiceclass from class-method-based architecture to instance-method-based architecture with unified dispatch and validation [0e939f5]SMTPEmailClienttemplate selection from dynamic method-based lookup to static mapping [0e939f5]COMPLETE_TASKfromEmailTypeenum and added five new constants toNotificationMethodenum with centralized title mapping [0e939f5]EmailServicemethods and assert with explicit user identifiers [0e939f5]EmailTemplateAdminhelp text to document additional template variables for auth, digests, and guest notifications [0e939f5]backend/src/templates/emails/user_deactivated.txttext email template file and regenerated migration to remove COMPLETE_TASK from email template choices [0e939f5]📊 Macroscope summarized 0e939f5. 19 files reviewed, 56 issues evaluated, 49 issues filtered, 1 comment posted
🗂️ Filtered Issues
backend/src/accounts/services/user.py — 0 comments posted, 2 evaluated, 2 filtered
send_user_deactivated_notification.delay(...)without error handling can raise at runtime (e.g., broker unavailable), causing the caller to fail after deactivation side effects have committed. This creates a partial-failure scenario where the user is deactivated but the request may return an error, with no retry/compensation or graceful degradation. [ Low confidence ]user.accountis dereferenced without a guard when passinglogo_lg=user.account.logo_lg. Ifuser.accountcan beNoneon any reachable call path to_deactivate_actions(e.g., if the method is invoked directly with a user lacking an account), this will raiseAttributeError. There is no local guard or constructor guarantee in this method establishing non-nullability. [ Low confidence ]backend/src/accounts/services/user_invite.py — 0 comments posted, 3 evaluated, 3 filtered
invite_uservia_transfer_existent_user->_send_transfer_email). If the transaction later rolls back, the email task still runs, causing side effects that do not reflect committed state. Usetransaction.on_commit(lambda: send_user_transfer_notification.delay(...))to ensure dispatch only after a successful commit. [ Low confidence ]self.request_useris optional per__init__(can beNone), but_send_transfer_emailunconditionally dereferences it (self.request_user.account_id,self.request_user.get_full_name(), andself.request_user.account.name). Ifrequest_userwas not provided, calling_send_transfer_emailwill raiseAttributeError, crashing the flow during invite/transfer or resend paths. [ Low confidence ]logo_lg=current_account_user.account.logo_lgis passed as a Celery task kwarg. Iflogo_lgis a DjangoImageField/FileField(i.e., aFieldFile), it is not JSON-serializable and Celery (with JSON serializer) will raiseTypeError: Object of type FieldFile is not JSON serializable. Convert to a primitive (e.g.,str(current_account_user.account.logo_lg.url or '')or name) before enqueueing. [ Low confidence ]backend/src/authentication/views/signin.py — 0 comments posted, 3 evaluated, 3 filtered
user.account.users.get(is_account_owner=True). If no owner exists or multiple owners exist, Django will raiseDoesNotExistorMultipleObjectsReturned, causing a 500 instead of a controlled error during sign-in for timed-out accounts. [ Low confidence ]send_verification_notification.delay(...). If the broker is unavailable or kwargs are not serializable, the call can raise and abort the request, preventing the intendedAuthenticationFailedresponse and leaking a 500. [ Low confidence ]logo_lg=user.account.logo_lginto a Celery task (send_verification_notification.delay(...)) may not be JSON-serializable (e.g.,ImageFieldFile). Celery (JSON serializer by default) will raise a serialization error at enqueue time, causing a 500. [ Already posted ]backend/src/authentication/views/signup.py — 0 comments posted, 2 evaluated, 2 filtered
send_verification_notification.delay(...)inafter_signup. Broker/serialization errors will raise and abort the request, potentially preventing subsequent logic (inc_anonymous_user_account_counter) and returning a 500. [ Low confidence ]logo_lg=account.logo_lgto the Celery task may not be JSON-serializable (e.g.,ImageFieldFile), leading to serialization errors at enqueue time and a 500 during signup completion. [ Already posted ]backend/src/authentication/views/verification.py — 0 comments posted, 3 evaluated, 3 filtered
request.user.account.users.get(is_account_owner=True). If the owner is missing or duplicated,.get()raises and the resend endpoint returns a 500. [ Low confidence ]send_verification_notification.delay(...)in the resend endpoint. Publish/serialization failures will raise and produce a 500 instead of returning the expected response. [ Low confidence ]logo_lg=user.account.logo_lgto the Celery task can fail JSON serialization (e.g.,ImageFieldFile), causing a 500 when resending verification. [ Low confidence ]backend/src/notifications/admin.py — 0 comments posted, 2 evaluated, 2 filtered
email_types(a PostgreSQLArrayField) inlist_filteris not supported by Django’s built-in admin filters and will raiseImproperlyConfiguredwhen loading the changelist.list_filterexpects fields likeBooleanField,DateField,ForeignKey, orCharFieldwithchoices; but anArrayFieldofCharField(choices=...)does not register a defaultFieldListFilter. Provide a customSimpleListFilteror a customFieldListFilterforArrayField, or remove this entry. [ Already posted ]email_types(a PostgreSQLArrayField) insearch_fieldswill cause database errors when the admin performs a search, because Django appliesicontainsby default, producing SQL likeemail_types ILIKE %...%, which is invalid fortext[](operator does not exist: text[] ILIKE ...). Removeemail_typesfromsearch_fieldsor implement a custom search viaget_search_resultsusing array-aware lookups (e.g.,__contains). [ Already posted ]backend/src/notifications/clients/customerio.py — 0 comments posted, 3 evaluated, 2 filtered
cio_template_ids[template_code]can beNoneif the corresponding environment variable is unset, resulting intransactional_message_id=None. This likely causes the downstreamcustomerioAPI call to fail at runtime. Add a check and return a clear error or skip sending when the template id is missing. [ Low confidence ]cio_template_ids[template_code]may raiseKeyErroriftemplate_codeis not one of the expected keys. TheEmailClient.send_emailcontract acceptstemplate_code: strwithout validation. Ensuretemplate_codeis validated or use.get(...)with explicit error handling to return a clear failure. [ Low confidence ]backend/src/notifications/clients/smtp.py — 1 comment posted, 9 evaluated, 7 filtered
self.connectionand uses it insend_emailwithout synchronization. Django’s email backends are not documented as thread-safe; reusing a single connection across concurrent calls can cause interleaved writes or backend state corruption. If the same client instance is used across threads, this can manifest as intermittent send failures. Use per-send connections, a connection pool, or ensure single-threaded use with locking. [ Low confidence ]__del__for connection cleanup is unsafe. At interpreter shutdown or in cyclic GC scenarios, module globals may beNoneand__del__may not run or may run when dependencies are already torn down, potentially raising exceptions or skipping cleanup. Do not rely on__del__for essential resource management; use explicit teardown or context management. [ Low confidence ]__del__may raise during interpreter shutdown or not run at all, making cleanup unreliable and potentially emitting exceptions to stderr. Module globals used by the connection/close path may beNoneat GC time. Relying on__del__for resource management is unsafe; prefer explicit close or context management. [ Low confidence ]tomay be empty or invalid (e.g.,''), leading to runtime failures from the email backend or SMTP server duringemail.send(). Add basic validation (non-empty, valid email format) or return a clear error before attempting to send. [ Low confidence ]_render_template_stringlacks error handling. Iftemplate.subjectortemplate.contentcontains invalid Django template syntax,Template(...)or.render(...)can raise (e.g.,TemplateSyntaxError), causingsend_emailto raise and abort without fallback, unlike the default-file template path which has a try/except and a fallback. Consider catching template exceptions here and falling back to_get_fallback_template. [ Low confidence ]self.connectionwithout synchronization. If a singleSMTPEmailClientinstance is used from multiple threads, concurrent calls tosend_emailwill operate on the same Django email backend connection (django.core.mail.backends.smtp.EmailBackend), which is not documented as thread-safe. This can cause interleaved writes or backend errors duringemail.send()when using the sharedconnection. Consider per-call connections, a connection pool, or locking around send operations. [ Low confidence ]_get_default_templatemay receivetemplate_name=None(whentemplate_codeis not present inDEFAULT_TEMPLATE_BY_TYPE), then callsget_template(template_name)which raises an exception that is immediately caught and ignored. This uses exceptions for normal control flow and hides genuine template-loading/rendering errors under the same catch-all, making diagnostics difficult and adding overhead. Guardtemplate_nameexplicitly and skip loader call when it is falsy; narrow the except to expected exceptions. [ Code style ]backend/src/notifications/services/email.py — 0 comments posted, 15 evaluated, 14 filtered
SMTPEmailClient: ifsettings.EMAIL_PROVIDERis any value other thanEmailProvider.CUSTOMERIO, the code selectsSMTPEmailClientwithout validation. This can route emails via the wrong provider with no error or log, violating configuration expectations. [ Code style ]_send_email_to_consoleassumesdatais a dict and callsdata.items()without validation. IfdataisNoneor any non-mapping type, this raisesAttributeError, crashing the call. Add a guard (e.g., default to{}or coerce/validate) beforedata.items()to ensure safe handling of empty or invalid inputs. [ Low confidence ]settings.PROJECT_CONF['EMAIL']without guarding against a missing key. IfPROJECT_CONFor theEMAILkey is absent or misconfigured at runtime,_sendwill raise, aborting all email sends instead of failing gracefully. [ Out of scope ]settings.PROJECT_CONF['EMAIL']without a default or existence check. IfPROJECT_CONFis missing theEMAILkey,_sendwill raise at runtime and abort sending/logging. Usegetwith a default (e.g.,settings.PROJECT_CONF.get('EMAIL', False)) or guard the key. [ Low confidence ]send_guest_new_task, whentask_due_dateis a string, it is parsed viaparse_datetimewithout validating the result. If parsing fails (returnsNone) or returns a naive datetime, the subsequent subtractiontask_due_date - timezone.now()will raiseTypeError(NoneType subtraction or naive/aware mismatch). Add validation and timezone normalization before arithmetic. [ Out of scope ]send_guest_new_task: Potential aware/naive datetime subtractiontask_due_date - timezone.now()can raiseTypeErroriftask_due_dateis naive (e.g., parsed from a string without timezone or passed as a naivedatetime). Normalize to aware timezone (or make both naive) before subtraction. [ Low confidence ]send_guest_new_task: Unsafe handling oftask_due_datewhen it is a string.parse_datetime(task_due_date)can returnNonefor unparseable inputs, causingTypeErroron subtraction atdue_in = task_due_date - timezone.now(). Add aNonecheck and handle invalid formats gracefully. [ Already posted ]send_workflows_digestcalls.strftime(...)ondate_from/date_towithout validation. IfNoneor a non-date/datetime is passed, it raisesAttributeError. Add type checks/coercion or stricter typing. [ Low confidence ]send_workflows_digestandsend_tasks_digestcall.strftime(...)ondate_fromanddate_towithout validating types. If either isNoneor not adate/datetime, this raisesAttributeError. Add type checks or conversions before formatting. [ Low confidence ]send_workflows_digestspreads**digestafter setting core fields indata. Any overlapping keys indigest(e.g.,unsubscribe_link,link,is_tasks_digest,status_labels) will silently override the intended values, potentially producing malformed or insecure email payloads. Place**digestfirst or sanitize/namespace its keys. [ Low confidence ]'/accounts/emails/unsubscribe?token={token}', butsend_tasks_digestuses'/accounts/unsubscribe/{token}'. This inconsistency can lead to broken links or mismatched backend routes. Align the URL format with the others or ensure both routes exist. [ Low confidence ]send_tasks_digestbuilds an unsubscribe URL inconsistent with other flows:'/accounts/unsubscribe/{token}'vs the established'/accounts/emails/unsubscribe?token=...'. If the former route does not exist (likely given other methods), users get a broken link. [ Low confidence ]send_tasks_digestcalls.strftime(...)ondate_from/date_towithout validating types. Non-date inputs (includingNone) will raiseAttributeError. Add strict typing and validation/coercion. [ Low confidence ]send_tasks_digestspreads**digestafter setting fixed fields indata. Overlapping keys indigestcan overwrite critical values (e.g., links, flags), producing malformed payloads. Move**digestearlier or whitelist allowed keys. [ Low confidence ]backend/src/reports/services/tasks.py — 0 comments posted, 3 evaluated, 3 filtered
_send_emailsassumes digests are finalized, but it does not callput_tmp()or sort templates before sending. When_process_datatriggers a bulk send (users_count > bulk_size), some users'TasksDigest.tmpmay still hold the last template, causing incomplete data to be sent._send_emailsshould finalize eachTasksDigest(e.g.,put_tmp()and required ordering) prior to enqueuing. [ Out of scope ]date_from(adate),date_to(adatetime), and especiallylogo_lg(likely a DjangoFieldFile) are included insend_tasks_digest_notification.delay(...). With Celery's default JSON serializer, this will raise a serialization error at enqueue time. Convert to primitives (e.g., ISO strings) or ensure a compatible serializer. [ Low confidence ]user.account.logo_lg: if the relatedaccountis missing/nullable,user.accountaccess will raiseRelatedObjectDoesNotExist. The code does not guard againstNone/missing relation before dereferencinglogo_lg. [ Low confidence ]backend/src/reports/services/workflows.py — 0 comments posted, 3 evaluated, 3 filtered
send_workflows_digest_notification.delay(...). Specifically:date_from(line 79) anddate_to(line 80) aredatetime.dateobjects, andlogo_lg(line 83) is likely a DjangoFieldFile/ImageFieldFile. With Celery's default JSON serializer these will raise a serialization error on the producer side, preventing the task from being enqueued. Even with pickle, passingFieldFileobjects is brittle and unnecessary. Convert dates to ISO strings and pass a primitive (e.g., URL or path) for the logo instead of the file object. [ Low confidence ]AttributeErrorifuser.accountisNonewhen accessinguser.account.logo_lg(line 83).select_related('account')does not guarantee the relation exists. Add a null check and provide a fallback (e.g., passNoneor a default logo URL). [ Low confidence ]user.last_digest_send_timeis updated (line 84) and_sent_digests_countincremented (line 86) immediately after enqueuing the task, without confirming the task actually executed or even reached the broker. If enqueueing fails or the worker fails to send the email, the user will still be marked as having received a digest, causing missed notifications and blocking retries. [ Low confidence ]backend/src/settings.py — 0 comments posted, 6 evaluated, 5 filtered
BACKEND_URL = env.get('BACKEND_URL')is assumed non-empty and well-formed, butBACKEND_HOST = BACKEND_URL.split('//')[1].split(':')[0]will raise ifBACKEND_URLis missing (None->AttributeError) or lacks'//'(IndexError). Add validation and a clear error or a safe default before splitting. [ Out of scope ]CORS_ORIGIN_WHITELIST = [FRONTEND_URL, FORMS_URL]can includeNoneif those env vars are unset. Some consumers (django-cors-headers) expect strings;Nonecan cause misconfiguration or errors during origin checks. Filter out falsy values before assigning or provide defaults. [ Out of scope ]EMAIL_BACKENDdefaulted to console; now whenEMAIL_PROVIDERis unset or not'customerio', it defaults to SMTP and requires additional envs (EMAIL_HOST, etc.). This can cause runtime failures in environments that previously worked without SMTP configuration (e.g., local dev) when sending mail. Consider preserving the prior default (console) or gating SMTP behind an explicitEMAIL_PROVIDER == 'smtp'. [ Low confidence ]EMAIL_PROVIDER == EmailProvider.CUSTOMERIO, the settings setEMAIL_BACKENDto'django.core.mail.backends.console.EmailBackend'(line 275). This backend only prints emails to the console and does not send them, which contradicts the implied contract of selecting thecustomerioprovider and will result in no emails being delivered in that mode. Given thatCUSTOMERIO_*API keys are configured immediately after, the code likely intended to use a Customer.io-capable backend or integration rather than the console backend. This is a runtime misconfiguration leading to silent non-delivery of emails wheneverEMAIL_PROVIDERis set to'customerio'. [ Low confidence ]EMAIL_USE_TLSandEMAIL_USE_SSLare set independently from env without enforcing mutual exclusivity. If both resolve to true, Django email configuration is invalid and may raise or fail to connect. Add a guard to ensure only one is enabled, with a clear error if both are set. [ Low confidence ]