seitime-frappe/frappe/email/smtp.py
Sagar Vora b3e1eda4c8
feat: global frappe.in_test flag (#32960)
* feat: global `frappe.in_test` flag

* feat: helper utility to toggle `frappe.in_test`

* fix: use `toggle_test_mode` util

* fix: use `frappe.in_test`

* chore: add comment explaining global `in_test`

* chore: ignore commit replacing flag usage

* test: temporarily disable `frappe.in_test`

this worked earlier because flag was set in werkzeug.local which was separate for API test client

* test: add comment explaining change
2025-06-17 19:19:31 +05:30

137 lines
3.3 KiB
Python

# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import smtplib
from contextlib import suppress
import frappe
from frappe import _
from frappe.email.oauth import Oauth
from frappe.utils import cint, cstr, get_traceback
class InvalidEmailCredentials(frappe.ValidationError):
pass
class SMTPServer:
def __init__(
self,
server,
login=None,
email_account=None,
password=None,
port=None,
use_tls=None,
use_ssl=None,
use_oauth=0,
access_token=None,
):
self.login = login
self.email_account = email_account
self.password = password
self._server = server
self._port = port
self.use_tls = use_tls
self.use_ssl = use_ssl
self.use_oauth = use_oauth
self.access_token = access_token
self._session = None
if not self.server:
frappe.msgprint(
_("Email Account not setup. Please create a new Email Account from Settings > Email Account"),
raise_exception=frappe.OutgoingEmailError,
)
@property
def port(self):
port = self._port or (self.use_ssl and 465) or (self.use_tls and 587)
return cint(port)
@property
def server(self):
return cstr(self._server or "")
def secure_session(self, conn):
"""Secure the connection incase of TLS."""
if self.use_tls:
conn.ehlo()
conn.starttls()
conn.ehlo()
@property
def session(self):
"""Get SMTP session.
We make best effort to revive connection if it's disconnected by checking the connection
health before returning it to user."""
if self.is_session_active():
return self._session
SMTP = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
try:
_session = SMTP(self.server, self.port, timeout=2 * 60)
if not _session:
frappe.msgprint(
_("Could not connect to outgoing email server"), raise_exception=frappe.OutgoingEmailError
)
self.secure_session(_session)
if self.use_oauth:
Oauth(_session, self.email_account, self.login, self.access_token).connect()
elif self.password:
res = _session.login(str(self.login or ""), str(self.password or ""))
# check if logged correctly
if res[0] != 235:
frappe.msgprint(res[1], raise_exception=frappe.OutgoingEmailError)
self._session = _session
self._enqueue_connection_closure()
return self._session
except smtplib.SMTPAuthenticationError:
self.throw_invalid_credentials_exception()
except OSError as e:
# Invalid mail server -- due to refusing connection
frappe.throw(
_("Invalid Outgoing Mail Server or Port: {0}").format(str(e)),
title=_("Incorrect Configuration"),
)
def _enqueue_connection_closure(self):
if frappe.request and hasattr(frappe.request, "after_response"):
frappe.request.after_response.add(self.quit)
elif frappe.job:
frappe.job.after_job.add(self.quit)
elif not frappe.in_test:
# Console?
import atexit
atexit.register(self.quit)
def is_session_active(self):
if self._session:
try:
return self._session.noop()[0] == 250
except Exception:
return False
def quit(self):
with suppress(TimeoutError):
if self.is_session_active():
self._session.quit()
@classmethod
def throw_invalid_credentials_exception(cls):
original_exception = get_traceback() or "\n"
frappe.throw(
_("Please check your email login credentials.") + " " + original_exception.splitlines()[-1],
title=_("Invalid Credentials"),
exc=InvalidEmailCredentials,
)