-
Notifications
You must be signed in to change notification settings - Fork 0
Adds email to API, adds SSL halon implementation #78
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
Merged
Merged
Changes from 5 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
07e6a48
Adds email service to api. Does not yet send any emails.
bdc34 d35e78e
Skipping test coverage for controllers cross and delete
bdc34 fc4b0e1
Adds Halon email implementation
bdc34 725188b
Adds new parameter to `FlaskSubmitImplementation.__init__`
bdc34 0b8627a
Merge branch 'develop' into bdc34/SUBMISSION-174-email-submitter
bdc34 9fc8f12
Adds sending of email to submitter.
bdc34 31a4117
Sends submit finalized confirmation to person who finalized
bdc34 c5f8084
Adds submission summary to finalize email
bdc34 4353333
Adds stubs for confirmation email body for types other than `new`
bdc34 a6d9c7f
Adds auto hold oversize email template
bdc34 8348335
Accept secret name for email config similar to how Publish works.
bdc34 9888927
email exceptions handled and recorded on Event property
bdc34 4861a7e
Fixes some email test failures
bdc34 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| """API for service to send email.""" | ||
| from abc import ABCMeta, abstractmethod | ||
|
|
||
| class EmailService(metaclass=ABCMeta): | ||
| @abstractmethod | ||
| def send_email(self, | ||
| to: list[str], | ||
| subject: str, | ||
| body: str, | ||
| reply_to: str, | ||
| cc: list[str] | None = None, | ||
| bcc: list[str] | None = None, | ||
| message_id: str="", | ||
| references: str="") -> None: | ||
| """Send an email. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| to : list[str] | ||
| Recipient email addresses. | ||
| subject : str | ||
| Subject line of the message. | ||
| body : str | ||
| Body content of the message. | ||
| reply_to : str | ||
| Address that replies should be directed to. | ||
| cc : list[str], optional | ||
| Addresses to copy on the message. Pass ``None`` or an empty list | ||
| for no CCs. | ||
| bcc : list[str], optional | ||
| Addresses to blind-copy on the message. Pass ``None`` or an empty | ||
| list for no BCCs. | ||
| message_id : str | ||
| Unique identifier for this message (the ``Message-ID`` header), | ||
| used for threading. | ||
| references : str | ||
| ``References`` header linking this message to prior messages in | ||
| a thread. | ||
| """ | ||
| pass | ||
|
|
||
|
|
||
|
|
||
| @abstractmethod | ||
| def is_available(self) -> bool: | ||
| """Determine whether the service is available.""" | ||
| pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| """Email service implementations. | ||
|
|
||
| `HalonEmailService` is the production `EmailService`. It dispatches mail | ||
| through arXiv's Halon SMTP server over an authenticated SMTP-over-SSL | ||
| connection, using the standard library ``smtplib``/``email`` modules. | ||
| """ | ||
| import logging | ||
| import smtplib | ||
| from email.message import EmailMessage | ||
| from email.utils import format_datetime, localtime, make_msgid | ||
| from typing import Optional | ||
|
|
||
| from typing_extensions import override | ||
|
|
||
| from submit_ce.api.email_service import EmailService | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class HalonEmailService(EmailService): | ||
| """`EmailService` that sends mail via arXiv's Halon SMTP server. | ||
|
|
||
| Connects to the Halon server over SMTP-over-SSL, authenticates, and | ||
| sends a single text-body message. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| host : str | ||
| Hostname of the Halon SMTP server. | ||
| user : str | ||
| Username for SMTP authentication. | ||
| password : str | ||
| Password for SMTP authentication. | ||
| from_address : str | ||
| Default ``From`` address for outgoing mail. | ||
| port : int | ||
| Port for the SMTP-over-SSL connection. Defaults to 465. | ||
| """ | ||
|
|
||
| def __init__(self, | ||
| host: str, | ||
| user: str, | ||
| password: str, | ||
| from_address: str, | ||
| port: int = 465) -> None: | ||
| self.host = host | ||
| self.user = user | ||
| self.password = password | ||
| self.from_address = from_address | ||
| self.port = port | ||
|
|
||
| def __repr__(self) -> str: | ||
| # Never include the password in a repr. | ||
| return (f"{self.__class__.__name__}(" | ||
| f"host={self.host!r}," | ||
| f"port={self.port!r}," | ||
| f"user={self.user!r}," | ||
| f"from_address={self.from_address!r}" | ||
| ")") | ||
|
|
||
| @override | ||
| def send_email(self, | ||
| to: list[str], | ||
| subject: str, | ||
| body: str, | ||
| reply_to: str, | ||
| cc: list[str] | None = None, | ||
| bcc: list[str] | None = None, | ||
| message_id: str = "", | ||
| references: str = "") -> None: | ||
| """Build and send a plain-text email through the Halon server. | ||
|
|
||
| Parameters match `EmailService.send_email`; see that method for | ||
| details. ``Bcc`` recipients are included in the SMTP envelope but | ||
| not in the message headers. | ||
| """ | ||
| cc = cc or [] | ||
| bcc = bcc or [] | ||
|
|
||
| msg = EmailMessage() | ||
| msg["Date"] = format_datetime(localtime()) | ||
| msg["Message-ID"] = message_id or make_msgid() | ||
| msg["From"] = self.from_address | ||
| msg["To"] = ", ".join(to) | ||
| msg["Subject"] = subject | ||
| if cc: | ||
| msg["Cc"] = ", ".join(cc) | ||
| if reply_to: | ||
| msg["Reply-To"] = reply_to | ||
| if references: | ||
| msg["References"] = references | ||
|
|
||
| # cte='8bit' and 8bitmime mail option avoids needless down-coding of the body. | ||
| # Recomended in halon docs. | ||
| msg.set_content(body, cte="8bit") | ||
| mail_options=("8bitmime",) | ||
|
|
||
| # Envelope recipients: every address that should receive the mail, | ||
| # including Bcc (which is intentionally absent from the headers). | ||
| recipients = list(to) + list(cc) + list(bcc) | ||
|
|
||
| with smtplib.SMTP_SSL(host=self.host, port=self.port) as sess: | ||
| sess.login(self.user, self.password) | ||
| sess.send_message(msg, | ||
| from_addr=self.from_address, | ||
| to_addrs=recipients, | ||
| mail_options=mail_options) | ||
| logger.info("Sent email %s to %d recipient(s)", | ||
| msg["Message-ID"], len(recipients)) | ||
|
|
||
| @override | ||
| def is_available(self) -> bool: | ||
| """Return ``True`` if the service has the config needed to send. | ||
|
|
||
| This checks configuration only; it does not open a connection to | ||
| the SMTP server. | ||
| """ | ||
| return bool(self.host and self.user and self.password | ||
| and self.from_address) | ||
|
|
||
|
|
||
| def email_service_from_settings(settings: Optional[object] = None) -> HalonEmailService: | ||
| """Build a `HalonEmailService` from application settings. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| settings : object, optional | ||
| Settings object exposing the ``EMAIL_*`` attributes. If ``None``, | ||
| the application's `submit_ce.ui.config.settings` is used. | ||
| """ | ||
| if settings is None: | ||
| from submit_ce.ui.config import settings as app_settings | ||
| settings = app_settings | ||
| return HalonEmailService( | ||
| host=settings.EMAIL_SMTP_HOST, | ||
| port=settings.EMAIL_SMTP_PORT, | ||
| user=settings.EMAIL_SMTP_USER, | ||
| password=settings.EMAIL_SMTP_PASSWORD, | ||
| from_address=settings.EMAIL_FROM, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| """In-memory `EmailInMemory` service for tests. | ||
|
|
||
| A drop-in `EmailService` for unit and integration tests that retains sent | ||
| messages in process memory instead of dispatching them through a real mail | ||
| transport. Tests can send email exactly as production code would, then inspect | ||
| `sent` (or use the convenience accessors) to assert on what was sent. | ||
| """ | ||
| from dataclasses import dataclass, field | ||
|
|
||
| from submit_ce.api.email_service import EmailService | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class SentEmail: # pragma: no cover | ||
| """A single captured email. | ||
|
|
||
| Mirrors the parameters of `EmailService.send_email`. | ||
| """ | ||
|
|
||
| to: list[str] | ||
| subject: str | ||
| body: str | ||
| reply_to: str | ||
| cc: list[str] = field(default_factory=list) | ||
| bcc: list[str] = field(default_factory=list) | ||
| message_id: str = "" | ||
| references: str = "" | ||
|
|
||
|
|
||
| class EmailInMemory(EmailService): # pragma: no cover | ||
| """An `EmailService` that captures sent email in memory for tests. | ||
|
|
||
| Each call to `send_email` appends a `SentEmail` to `sent`, in order. The | ||
| captured messages can then be examined to make assertions during a test. | ||
|
|
||
| Examples | ||
| -------- | ||
| >>> service = EmailInMemory() | ||
| >>> service.send_email(["a@example.com"], "Hi", "Body", "noreply@arxiv.org") | ||
| >>> len(service.sent) | ||
| 1 | ||
| >>> service.last.subject | ||
| 'Hi' | ||
| >>> service.clear() | ||
| >>> service.sent | ||
| [] | ||
| """ | ||
|
|
||
| def __init__(self) -> None: | ||
| self.sent: list[SentEmail] = [] | ||
|
|
||
| def send_email(self, | ||
| to: list[str], | ||
| subject: str, | ||
| body: str, | ||
| reply_to: str, | ||
| cc: list[str] | None = None, | ||
| bcc: list[str] | None = None, | ||
| message_id: str = "", | ||
| references: str = "") -> None: | ||
| """Capture an email in memory. | ||
|
|
||
| Parameters match `EmailService.send_email`; see that method for | ||
| details. The message is appended to `sent` rather than dispatched. | ||
| """ | ||
| self.sent.append(SentEmail( | ||
| to=list(to), | ||
| subject=subject, | ||
| body=body, | ||
| reply_to=reply_to, | ||
| cc=list(cc or []), | ||
| bcc=list(bcc or []), | ||
| message_id=message_id, | ||
| references=references, | ||
| )) | ||
|
|
||
| @property | ||
| def last(self) -> SentEmail: | ||
| """Return the most recently sent email. | ||
|
|
||
| Raises | ||
| ------ | ||
| IndexError | ||
| If no email has been sent. | ||
| """ | ||
| return self.sent[-1] | ||
|
|
||
| def clear(self) -> None: | ||
| """Discard all captured emails.""" | ||
| self.sent.clear() | ||
|
|
||
| def is_available(self) -> bool: | ||
| return True |
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.