Coverage for functions \ flipdare \ mailer \ admin_mailer.py: 100%
0 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python
2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
13from __future__ import annotations
15import smtplib
16from typing import Any, override
18import flask
20from flipdare.mailer.email_image import EmailImage
21from flipdare.app_globals import is_valid_email
22from flipdare.app_log import LOG
23from flipdare.constants import IS_DEBUG, IS_TRACE
24from flipdare.error.stack_util import StackUtil
25from flipdare.generated.schema.email.body.admin.log_email_schema import LogEmailSchema
26from flipdare.mailer.admin.app_contact_email import AppContactEmail
27from flipdare.mailer.admin.app_log_email import AppLogEmail
28from flipdare.mailer._jinja_email_template import JinjaEmailTemplate
29from flipdare.mailer._mailer import Mailer, MailerOptions
30from flipdare.error.error_context import ErrorContext
31from flipdare.generated import AppErrorCode
32from flipdare.generated.shared.app_log_category import AppLogCategory
33from flipdare.generated.shared.backend.app_job_type import AppJobType
34from flipdare.generated.shared.backend.system_log_type import SystemLogType
35from flipdare.generated.shared.firestore_collections import FirestoreCollections
36from flipdare.generated.shared.search.search_collections import SearchCollections
37from flipdare.message.user_message import UserMessage
38from flipdare.request.data.search_request_adapter import AppRequest
39from flipdare.core.app_response import AppErrorResponse, AppOkResponse
40from flipdare.util.debug_util import stringify_debug
41from flipdare.util.time_util import TimeUtil
43__all__ = ["AdminMailer"]
46class AdminMailer(Mailer):
47 def __init__(
48 self,
49 options: MailerOptions | None = None,
50 smtp_client: smtplib.SMTP_SSL | None = None,
51 ) -> None:
52 super().__init__(options=options, smtp_client=smtp_client)
54 @override
55 def send( # type: ignore[override]
56 self,
57 email_template: JinjaEmailTemplate[Any],
58 images: list[EmailImage] | None = None,
59 ) -> bool: # pragma: no cover
60 """Send admin email using SMTP."""
61 email = self.options.internal_service_email
62 if IS_TRACE:
63 msg = f"Preparing to send admin email to {email} with template {email_template.__class__.__name__}"
64 LOG().trace(msg)
66 # dont minify admin messages, easier to troubleshoot!
67 return self._send_template(email, email_template, should_minify=False, images=images)
69 def send_contact(self, request: AppRequest[Any]) -> flask.Response:
70 # this is remote so we need to check the honeypot_field
71 # If honeypot_field is NOT empty: Reject the submission immediately
72 # (do not process the unsubscribe request).
73 # If honeypot_field IS empty: Proceed with normal processing.
74 from flipdare.services import get_app_logger
76 req = request.raw_request
77 url = req.url
79 if req.method.lower() != "post":
80 msg = f"Invalid method ({req.method}) for unsubscribe"
81 return AppErrorResponse.from_context(
82 ErrorContext.forbidden(url, message=msg),
83 ).raw_response()
85 if IS_TRACE:
86 LOG().trace(
87 f"Received contact email with details:\n"
88 f"URL: {req.url}\n"
89 f"Method: {req.method}\n"
90 f"Headers: {req.headers}\n"
91 f"Body: {req.get_data(as_text=True)}",
92 )
94 email = None
95 try:
96 data = req.get_json()
97 honeypot_field = data.get("pot_field", "")
98 if honeypot_field:
99 # if spam, return ok but with an empty message and do nothing..
100 if IS_DEBUG:
101 LOG().debug(f"Spam detected in contact form for email {email}")
103 return AppOkResponse.ok().raw_response()
105 email = data.get("email", "")
106 subject = data.get("subject", "")
107 message = data.get("message", "")
108 except Exception as e:
109 msg = f"Contact error: {e}"
110 return AppErrorResponse.message(
111 url=url,
112 error_code=AppErrorCode.INVALID_DATA,
113 message=msg,
114 ).raw_response()
116 email_result = is_valid_email(email)
117 if email_result.is_error:
118 msg = f"Invalid email address: {email}"
119 return AppErrorResponse.message(
120 url=url,
121 error_code=AppErrorCode.INVALID_EMAIL,
122 message=msg,
123 ).raw_response()
125 email_content = AppContactEmail(from_email=email, message=message, from_subject=subject)
127 if IS_TRACE:
128 LOG().trace(f"Sending contact email for {email}")
129 try:
130 # first we send the email, and update if the email is sent ok
131 self.send(email_template=email_content)
132 if IS_TRACE:
133 LOG().trace(f"Contact email sent to admin for {email}")
135 return AppOkResponse.message(UserMessage.CONTACT_OK).raw_response()
136 except Exception as error:
137 cause = f"Error sending contact email to admin for {email}: {error}"
138 get_app_logger().contact_email_error(
139 error_code=AppErrorCode.CONTACT,
140 email=email,
141 error=error,
142 message=cause,
143 )
145 return AppErrorResponse.message(
146 url=url,
147 error_code=AppErrorCode.SERVER,
148 message=cause,
149 ).raw_response()
151 def send_error(
152 self,
153 error_code: str,
154 message: str,
155 job_type: AppJobType | None = None,
156 collection: FirestoreCollections | SearchCollections | None = None,
157 category: AppLogCategory | None = None,
158 include_stack: bool = True,
159 ) -> None:
160 source = job_type or collection or category or "UnknownSource"
161 schema = LogEmailSchema(
162 occurred_at=TimeUtil.formatted_now(),
163 called_from=StackUtil.get_caller_str(),
164 source=source,
165 log_type=SystemLogType.ERROR,
166 error_code=error_code,
167 message=message,
168 )
170 if job_type is not None:
171 schema["job_type"] = job_type
172 if include_stack:
173 schema["stack_trace"] = StackUtil.get_flipdare_stack()
175 self.send_log(schema)
177 def send_log(
178 self,
179 schema: LogEmailSchema,
180 ) -> None:
181 try:
182 template = AppLogEmail(schema)
183 self.send(template)
184 except Exception as ex:
185 # NOTE: the primary reason for this failure is SMTP issues, so just log
186 msg = f"Exception thrown sending email: {ex}\n"
187 msg += f"Schema:\n{stringify_debug(schema)}"
188 LOG().error(msg)