diff --git a/README.md b/README.md index d846d486..b8530135 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,7 @@ To generate coverage reports for the Javascript tests: - `MAIL_SENDER`: String. Email address to send outgoing mail from. - `MAIL_SERVER`: The SMTP host - `MAIL_TLS`: Boolean. Use TLS to connect to the SMTP server. +- `MICROSOFT_TASK_ORDER_EMAIL_ADDRESS`: String. Email address for Microsoft to receive PDFs of new and updated task orders. - `PERMANENT_SESSION_LIFETIME`: Integer specifying how many seconds a user's session can stay valid for. https://flask.palletsprojects.com/en/1.1.x/config/#PERMANENT_SESSION_LIFETIME - `PGDATABASE`: String specifying the name of the postgres database. - `PGHOST`: String specifying the hostname of the postgres database. diff --git a/atst/utils/mailer.py b/atst/utils/mailer.py index a67a851b..a5dbfc0b 100644 --- a/atst/utils/mailer.py +++ b/atst/utils/mailer.py @@ -1,5 +1,6 @@ from contextlib import contextmanager import smtplib +import io from email.message import EmailMessage @@ -76,8 +77,34 @@ class Mailer(object): return msg - def send(self, recipients, subject, body): + def _add_attachment(self, message, content, filename, maintype, subtype): + with io.BytesIO(content) as bytes_: + message.add_attachment( + bytes_.read(), filename=filename, maintype=maintype, subtype=subtype + ) + + def send(self, recipients, subject, body, attachments=[]): + """ + Send a message, optionally with attachments. + Attachments should be provided as a list of dictionaries of the form: + { + content: bytes, + maintype: string, + subtype: string, + filename: string, + } + """ message = self._build_message(recipients, subject, body) + if attachments: + message.make_mixed() + for attachment in attachments: + self._add_attachment( + message, + content=attachment["content"], + filename=attachment["filename"], + maintype=attachment.get("maintype", "application"), + subtype=attachment.get("subtype", "octet-stream"), + ) self.connection.send(message) @property diff --git a/config/base.ini b/config/base.ini index 71081e83..1f4c732a 100644 --- a/config/base.ini +++ b/config/base.ini @@ -26,6 +26,7 @@ MAIL_PORT MAIL_SENDER MAIL_SERVER MAIL_TLS +MICROSOFT_TASK_ORDER_EMAIL_ADDRESS = example@example.com PERMANENT_SESSION_LIFETIME = 1800 PGDATABASE = atat PGHOST = localhost diff --git a/tests/conftest.py b/tests/conftest.py index 8c22ddc3..03d07a12 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -164,6 +164,12 @@ def pdf_upload2(): yield FileStorage(fp, content_type="application/pdf") +@pytest.fixture +def downloaded_task_order(): + with open(PDF_FILENAME, "rb") as fp: + yield {"name": "mock.pdf", "content": fp.read()} + + @pytest.fixture def extended_financial_verification_data(pdf_upload): return { diff --git a/tests/utils/test_mailer.py b/tests/utils/test_mailer.py index 8b40ea48..41c68434 100644 --- a/tests/utils/test_mailer.py +++ b/tests/utils/test_mailer.py @@ -1,10 +1,17 @@ import pytest -from atst.utils.mailer import Mailer, Mailer, MailConnection, RedisConnection +from atst.utils.mailer import ( + Mailer, + MailConnection, + RedisConnection, +) +from atst.utils.localization import translate +from email.mime.base import MIMEBase class MockConnection(MailConnection): def __init__(self): self._messages = [] + self.sender = "mock@mock.com" def send(self, message): self._messages.append(message) @@ -46,3 +53,55 @@ def test_redis_mailer_can_save_messages(app): assert message_data["recipients"][0] in message assert message_data["subject"] in message assert message_data["body"] in message + + +def test_send_with_attachment(app, mailer, downloaded_task_order): + to_number = "11111111111111" + attachment = { + "maintype": "application", + "subtype": "pdf", + "filename": downloaded_task_order["name"], + "content": downloaded_task_order["content"], + } + mailer.send( + recipients=[app.config["MICROSOFT_TASK_ORDER_EMAIL_ADDRESS"]], + subject=translate("email.task_order_sent.subject", {"to_number": to_number}), + body=translate("email.task_order_sent.body", {"to_number": to_number}), + attachments=[attachment], + ) + # one email was sent + assert len(mailer.messages) == 1 + + # the email was sent to Microsoft with the correct subject line + message = mailer.messages[0] + assert message["To"] == app.config["MICROSOFT_TASK_ORDER_EMAIL_ADDRESS"] + assert message["Subject"] == translate( + "email.task_order_sent.subject", {"to_number": to_number} + ) + + # the email was sent as a multipart message with two parts -- the message + # body and the attachment + assert message.is_multipart() + message_payload = message.get_payload() + assert len(message_payload) == 2 + + # A body and attachment were in the email + body = next( + ( + part + for part in message_payload + if part["Content-Type"] == 'text/plain; charset="utf-8"' + ), + None, + ) + attachment = next( + (part for part in message_payload if part["Content-Type"] == "application/pdf"), + None, + ) + assert body + assert attachment + + assert ( + attachment["Content-Disposition"] + == f"attachment; filename=\"{downloaded_task_order['name']}\"" + ) diff --git a/translations.yaml b/translations.yaml index 4fdb3b3c..44dd2a92 100644 --- a/translations.yaml +++ b/translations.yaml @@ -84,6 +84,9 @@ email: application_invite: "{inviter_name} has invited you to a JEDI cloud application" portfolio_invite: "{inviter_name} has invited you to a JEDI cloud portfolio" environment_ready: JEDI cloud environment ready + task_order_sent: + subject: "Task Order {to_number}" + body: "Task Order number {to_number} updated." empty_state: applications: header: @@ -480,7 +483,6 @@ portfolios: "False": View Team "True": Edit Team perms_env_mgmt: - "False": View Environments "True": Edit Environments roles: