Source code for yagmail.sender

# when there is a bcc a different message has to be sent to the bcc
# person, to show that they are bcc'ed

import logging
import smtplib
import time
from typing import Any, Dict, List, Literal, Optional, Tuple, Type, Union

from yagmail.dkim import DKIM
from yagmail.headers import AddressInput, make_addr_alias_user, resolve_addresses
from yagmail.log import get_logger
from yagmail.message import prepare_message
from yagmail.oauth2 import get_oauth2_info, get_oauth_string
from yagmail.password import handle_password
from yagmail.utils import find_user_home_path
from yagmail.validate import validate_email_with_regex


[docs] class Client: """ :class:`yagmail.Client` is a magic wrapper around ``smtplib``'s SMTP connection, and allows messages to be sent.""" def __init__( self, user: Optional[str] = None, password: Optional[Union[str, Dict[str, Any]]] = None, host: str = "smtp.gmail.com", port: Optional[Union[int, str]] = None, smtp_starttls: Optional[Union[bool, dict]] = None, smtp_ssl: bool = True, smtp_set_debuglevel: int = 0, smtp_skip_login: bool = False, encoding: str = "utf-8", oauth2_file: Optional[str] = None, soft_email_validation: bool = True, dkim: Optional[DKIM] = None, **kwargs: Any ): self.log = get_logger() self.set_logging() self.soft_email_validation = soft_email_validation if oauth2_file is not None: oauth2_info = get_oauth2_info(oauth2_file, user) if user is None: user = oauth2_info["email_address"] if smtp_skip_login and user is None: user = "" elif user is None: user = find_user_home_path() if user is None: raise ValueError( "No user provided. Pass `user=` to Client(), or create ~/.yagmail " "containing your email address." ) self.user, self.useralias = make_addr_alias_user(user) if soft_email_validation: validate_email_with_regex(self.user) self.is_closed: Optional[bool] = None self.host = host self.port = str(port) if port is not None else "465" if smtp_ssl else "587" self.smtp_starttls = smtp_starttls self.ssl = smtp_ssl self.smtp_skip_login = smtp_skip_login self.debuglevel = smtp_set_debuglevel self.encoding = encoding self.kwargs = kwargs self.cache: Dict[Any, Any] = {} self.unsent: List[Tuple[List[str], str]] = [] self.num_mail_sent = 0 self.oauth2_file = oauth2_file self.credentials = password if oauth2_file is None else oauth2_info self.dkim = dkim self.smtp: Union[smtplib.SMTP, smtplib.SMTP_SSL] = None # type: ignore def __enter__(self) -> "Client": return self def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Literal[False]: if not self.is_closed: self.close() return False @property def connection(self) -> Union[Type[smtplib.SMTP_SSL], Type[smtplib.SMTP]]: return smtplib.SMTP_SSL if self.ssl else smtplib.SMTP @property def starttls(self) -> Union[bool, dict]: if self.smtp_starttls is None: return False if self.ssl else True return self.smtp_starttls
[docs] def set_logging( self, log_level: Optional[int] = logging.ERROR, file_path_name: Optional[str] = None ) -> None: """ This function allows to change the logging backend, either output or file as backend It also allows to set the logging level (whether to display only critical/error/info/debug. for example:: yag = yagmail.Client() yag.set_logging(yagmail.logging.DEBUG) # to see everything and:: yagmail.set_logging(yagmail.logging.DEBUG, 'somelocalfile.log') lastly, a log_level of :py:class:`None` will make sure there is no I/O. """ self.log = get_logger(log_level, file_path_name)
def prepare_send( self, to: Optional[AddressInput] = None, subject: Optional[Union[str, List[str]]] = None, contents: Optional[Any] = None, attachments: Optional[Any] = None, cc: Optional[AddressInput] = None, bcc: Optional[AddressInput] = None, headers: Optional[Dict[str, str]] = None, prettify_html: bool = True, message_id: Optional[str] = None, group_messages: bool = True, ) -> Tuple[List[str], str]: addresses = resolve_addresses(self.user, self.useralias, to, cc, bcc) if self.soft_email_validation: for email_addr in addresses["recipients"]: validate_email_with_regex(email_addr) msg = prepare_message( self.user, self.useralias, addresses, subject, contents, attachments, headers, self.encoding, prettify_html, message_id, group_messages, self.dkim, ) recipients = addresses["recipients"] msg_strings = msg.as_string() return recipients, msg_strings
[docs] def send( self, to: Optional[AddressInput] = None, subject: Optional[Union[str, List[str]]] = None, contents: Optional[Any] = None, attachments: Optional[Any] = None, cc: Optional[AddressInput] = None, bcc: Optional[AddressInput] = None, preview_only: bool = False, headers: Optional[Dict[str, str]] = None, prettify_html: bool = True, message_id: Optional[str] = None, group_messages: bool = True, ) -> Union[Tuple[List[str], str], Dict[str, Any], bool]: """ Use this to send an email with gmail""" self.login() recipients, msg_strings = self.prepare_send( to, subject, contents, attachments, cc, bcc, headers, prettify_html, message_id, group_messages, ) if preview_only: return recipients, msg_strings return self._attempt_send(recipients, msg_strings)
def _attempt_send(self, recipients: List[str], msg_strings: str) -> Union[Dict[str, Any], bool]: attempts = 0 while attempts < 3: try: result = self.smtp.sendmail(self.user, recipients, msg_strings) self.log.info("Message sent to %s", recipients) self.num_mail_sent += 1 return result except smtplib.SMTPServerDisconnected as e: self.log.error(e) attempts += 1 if attempts < 3: try: self.login() except Exception as reconnect_err: self.log.error("Failed to reconnect during retry: %s", reconnect_err) time.sleep(attempts * 3) self.unsent.append((recipients, msg_strings)) return False
[docs] def send_unsent(self) -> None: """ Emails that were not being able to send will be stored in :attr:`self.unsent`. Use this function to attempt to send these again """ for i in range(len(self.unsent)): recipients, msg_strings = self.unsent.pop(i) self._attempt_send(recipients, msg_strings)
[docs] def close(self) -> None: """ Close the connection to the SMTP server """ self.is_closed = True try: self.smtp.quit() except (TypeError, AttributeError, smtplib.SMTPServerDisconnected): pass
[docs] def login(self) -> None: """ Connect and login to the SMTP server. """ if self.oauth2_file is not None: if isinstance(self.credentials, dict): self._login_oauth2(self.credentials) else: raise TypeError("OAuth2 credentials must be a dictionary") else: self._login(self.credentials)
def _login(self, password: Any) -> None: """ Login to the SMTP server using password. `login` only needs to be manually run when the connection to the SMTP server was closed by the user. """ self.smtp = self.connection(self.host, self.port, **self.kwargs) # type: ignore self.smtp.set_debuglevel(self.debuglevel) if self.starttls: self.smtp.ehlo() if self.starttls is True: self.smtp.starttls() else: self.smtp.starttls(**self.starttls) # type: ignore self.smtp.ehlo() self.is_closed = False if not self.smtp_skip_login: password = self.handle_password(self.user, password) self.smtp.login(self.user, password) self.log.info("Connected to SMTP @ %s:%s as %s", self.host, self.port, self.user) @staticmethod def handle_password(user: str, password: Optional[str]) -> str: return handle_password(user, password) @staticmethod def get_oauth_string(user: str, oauth2_info: Dict[str, Any]) -> str: return get_oauth_string(user, oauth2_info) def _login_oauth2(self, oauth2_info: Dict[str, Any]) -> None: if "email_address" in oauth2_info: oauth2_info = oauth2_info.copy() oauth2_info.pop("email_address") self.smtp = self.connection(self.host, self.port, **self.kwargs) # type: ignore try: self.smtp.set_debuglevel(self.debuglevel) except AttributeError: pass auth_string = self.get_oauth_string(self.user, oauth2_info) self.smtp.ehlo(oauth2_info["google_client_id"]) if self.starttls is True: self.smtp.starttls() self.smtp.docmd("AUTH", "XOAUTH2 " + auth_string)
[docs] def feedback( self, message: str = "Awesome features! You made my day! How can I contribute?", ) -> None: """ Most important function. Please send me feedback :-) """ self.send("kootenpv@gmail.com", "Yagmail feedback", message)
def __del__(self) -> None: try: if not self.is_closed: self.close() except AttributeError: pass
SMTP = Client