# Email Notification for Treatments 📬 In the treatment request form, users can check a **Notify by email** box to receive a message on the email address linked to the account when the task is completed. At the end of the [task workflow](https://github.com/Aikon-platform/aikon-api/wiki#task-workflow), the user receives an email about the exit status of the requested treatment (SUCCESS or ERROR), and additional logging information if the task exited with an error. ![Screenshot from 2025-04-28 11-23-38](https://github.com/user-attachments/assets/6ab4c7b8-d02f-4ed6-8d5d-c1ec10af9290) ## SMTP Server Setup ### Gmail Configuration To handle the mail transfer, we rely on the Gmail SMTP server to avoid the emails being classified as spam. **A Gmail address is required to proceed.** - Enable [two-step authentication](https://safety.google/security/security-tips/) for the account you wish to use - Create an [app password](https://myaccount.google.com/apppasswords) linked to your account The generated password is used in the platform `.env` file. ```python # SMTP server domain EMAIL_HOST=smtp.gmail.com # Email address to send alert emails EMAIL_HOST_USER=your_address@gmail.com # App password for email address EMAIL_HOST_PASSWORD=yourapppasswordwithoutspaces ``` #### Troubleshooting **Connection unexpectedly closed:** Add permit to firewall ```bash $ telnet [smtp.gmail.com](http://smtp.gmail.com/) 587 Trying 64.233.167.108... Connected to [smtp.gmail.com](http://smtp.gmail.com/). Escape character is '^]'. Connection closed by foreign host. ``` Ask the webmaster to allow to in firewall **Proxy resolution:** configure `extra_host` See : [Mapping SMTP server to IP](https://github.com/Aikon-platform/aikon/wiki/Docker-deploy#configure-extra_hosts) ### Institutional SMTP Server > 🚧 under construction Test if the SMTP port is accessible (common ports: `25`, `465`, `587`, `2525`): ```bash telnet localhost 25 ``` Once connected, send a test email by entering each line sequentially: ``` EHLO localhost MAIL FROM: RCPT TO: DATA Subject: Test Test message . QUIT ``` Replace `noreply@your-institution.domain` with your institution's sender address (e.g. `noreply@enpc.fr`) and `your.email@domain.com` with your actual email. If the test email is received, add to `docker-compose.yml`: ```yml web: extra_hosts: - "smtp:host-gateway" ``` then configure `.env` with your SMTP settings: ```bash EMAIL_HOST=smtp EMAIL_PORT=25 # or your working port EMAIL_HOST_USER= # leave empty EMAIL_HOST_PASSWORD= # leave empty DEFAULT_FROM_EMAIL=noreply@your-institution.domain SERVER_EMAIL=your.email@domain.com ``` Inside 'docker/` folder, try configuration with ``` docker compose exec web /home/aikon/venv/bin/python /home/aikon/app/manage.py test_email --to your.email@domain.com ``` #### Troubleshooting **Connection timeout:** Allow container traffic through firewall ```bash # Get container subnet docker network inspect aikondemo_demo_network --format '{{(index .IPAM.Config 0).Subnet}}' # Allow traffic from container subnet (replace with actual address) sudo ufw allow in from to any port 25 proto tcp ``` **Connection still failing:** Configure Postfix `sudo vi /etc/postfix/main.cf` ```conf inet_interfaces = all mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 ... ... ... ``` Inside the `docker/` folder, restart services and test email configuration ```bash sudo systemctl restart postfix bash docker.sh restart docker compose exec web /home/aikon/venv/bin/python /home/aikon/app/manage.py test_email --to your.email@domain.com ``` ## Email variables In the [`.env` file](https://github.com/Aikon-platform/aikon/blob/d6f0607174e91affb73084accf6c522ba5c93dee/front/app/config/.env.template#L85), update the environment variables with your configuration. ```python # Gmail uses 587 for TLS, 465 for SSL EMAIL_PORT="587" # SMTP server domain EMAIL_HOST=smtp.gmail.com # Email address to send alert emails EMAIL_HOST_USER=app_name@mail.com # App password for email address EMAIL_HOST_PASSWORD=yourapppassword ``` - `EMAIL_HOST`: SMTP server used for sending email. For Gmail, use `smtp.gmail.com` - `EMAIL_HOST_USER`: Username for the SMTP server. For Gmail, use the Gmail address used to generate the app password - `EMAIL_HOST_PASSWORD`: Password for the SMTP server. For Gmail, use the app password generated in the next step In the platform [settings](https://github.com/Aikon-platform/aikon/tree/main/front/app/config/settings), the Django email back end is defined by the `EMAIL_BACKEND` variable. - `EMAIL_BACKEND`: Handles the sending of an email. AIKON uses the default Django email back end ([official documentation](https://docs.djangoproject.com/en/5.1/topics/email/#topic-email-backends)) #### `dev.py` ```python EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" ``` #### `prod.py` In production, default variables are defined in the [`prod.py` settings file](https://github.com/Aikon-platform/aikon/blob/main/front/app/config/settings/prod.py). - `EMAIL_USE_TLS`: **True** to use TLS connection when back end dialogs with SMTP server. - `EMAIL_PORT`: Port 587 is attributed to the email server, but another port can be set by adding a `EMAIL_PORT` variable in the `.env` file. ```python EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_USE_TLS = True EMAIL_HOST = ENV("EMAIL_HOST", default="localhost") EMAIL_PORT = ENV("EMAIL_PORT", default=587) EMAIL_HOST_USER = ENV("EMAIL_HOST_USER") EMAIL_HOST_PASSWORD = ENV("EMAIL_HOST_PASSWORD", default="") ``` ### Email Address The `EMAIL_HOST_USER` defined in the `.env` is used: - As contact email for the superadmin - To receive logging alerts from Django in production - To send automatic emails from the platform (password reset, task completion...) - For users to contact the team for support A default `CONTACT_MAIL` based on the `EMAIL_HOST_USER` is defined in the `base.py`. To set a different contact email from the host user, a `CONTACT_EMAIL` variable can be added to the `.env`. ```python EMAIL_HOST_USER = ENV("EMAIL_HOST_USER") CONTACT_MAIL = ENV("CONTACT_EMAIL", default=EMAIL_HOST_USER) ``` In the `prod.py` settings file, the `ADMIN_EMAIL` variable is defined based on this `CONTACT_EMAIL`. ```python ADMIN_EMAIL = CONTACT_MAIL ADMINS = [(f"{APP_NAME} admin", ADMIN_EMAIL)] ``` ## Implementation: [`treatment.py`](https://github.com/Aikon-platform/aikon/blob/main/front/app/webapp/models/treatment.py) The email notification functionalities are linked to the [`Treatment` model](https://github.com/Aikon-platform/aikon/blob/d6f0607174e91affb73084accf6c522ba5c93dee/front/app/webapp/models/treatment.py#L47). For each treatment instance, the user can request an email notification through a boolean field. ```python class Treatment(AbstractSearchableModel): class Meta: ... notify_email = models.BooleanField( default=True, verbose_name=get_name("notify_email"), blank=True, help_text="Send an email when the task is finished" if APP_LANG == "en" else "Envoyer un email lorsque la tâche est terminée", ) ``` This information is circulated as a parameter of the request sent to the API, and returned to the front with the task results. After the [results are processed](https://github.com/Aikon-platform/aikon/blob/d6f0607174e91affb73084accf6c522ba5c93dee/front/app/webapp/models/treatment.py#L300), this information is used to send an email to the user if requested, with either [a success or an error message](https://github.com/Aikon-platform/aikon/blob/d6f0607174e91affb73084accf6c522ba5c93dee/front/app/webapp/models/treatment.py#L328). ```python def on_task_success(self, data, request=None): """ Handle the end of the task """ self.terminate_task( "SUCCESS", message=data.get("message"), notify=data.get("notify") ) if request: flash_msg = ( f"The requested task was completed.\n{data.get('message', '')}" if APP_LANG == "en" else f"La tâche demandée a été complétée.\n{data.get('message', '')}" ) messages.warning(request, flash_msg) def on_task_error( self, data, request=None, exception: Exception = None, completed=True ): """ Handle the end of the task """ log(data.get("error", "Unknown error"), exception=exception) if completed: self.terminate_task( "ERROR", error=data.get("error", "Unknown error"), notify=data.get("notify"), ) if request: flash_msg = ( f"The requested task encountered an error during execution." f"\n{data.get('error', 'Unknown error')}" if APP_LANG == "en" else f"La tâche demandée a rencontré une erreur lors de son exécution." f"\n{data.get('error', 'Unknown error')}" ) messages.warning(request, flash_msg) ``` The content of the email is defined in the [`terminate_task()` method](https://github.com/Aikon-platform/aikon/blob/d6f0607174e91affb73084accf6c522ba5c93dee/front/app/webapp/models/treatment.py#L369). It provides information on the treatment (type, id number), and provides the users with the error message if the treatment exited with an error. ```python msg = f"\n\nMessage: {error or message}." if (error or message) else "" email = ( f"Dear {APP_NAME.upper()} user,\n\n" f"The {self.task_type} task (#{self.id}) you requested on the {APP_NAME.upper()} platform " f"was completed with the status {self.status}.{msg}" f"\n\nBest,\nthe {APP_NAME.upper()} team." ) ``` The email is sent using the `send_mail()` function from Django email package. The email is sent to the email address of the user who requested the treatment, from the `CONTACT_MAIL` defined in the settings. ```python send_mail( f"[{APP_NAME.upper()} {self.task_type}] Task {self.status.lower()}", email, CONTACT_MAIL, [self.requested_by.email], fail_silently=False, ) ``` ⚠️ If no email address is attached to the user account, an error is logged.