Coverage for lib/utils/mail.py: 65%
71 statements
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-28 07:25 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-28 07:25 +0000
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
4# Hermes : Change Data Capture (CDC) tool from any source(s) to any target
5# Copyright (C) 2023, 2024 INSA Strasbourg
6#
7# This file is part of Hermes.
8#
9# Hermes is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# Hermes is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with Hermes. If not, see <https://www.gnu.org/licenses/>.
22from typing import Any
24from lib.config import HermesConfig
26from email.message import EmailMessage
27from dataclasses import dataclass
28import difflib
29import gzip
30import smtplib
33@dataclass
34class Attachment:
35 filename: str
36 mimetype: str
37 content: bytes
39 def __len__(self) -> int:
40 return len(self.content)
42 @property
43 def maintype(self) -> str:
44 return self.mimetype.split("/", 1)[0]
46 @property
47 def subtype(self) -> str:
48 return self.mimetype.split("/", 1)[1]
51class Email:
52 """Helper class to send mails"""
54 @staticmethod
55 def send(
56 config: HermesConfig,
57 subject: str,
58 content: str,
59 attachments: list[Attachment] = [],
60 ):
61 """Send a mail with specified subject and content, using "server",
62 "from" and "to" set in specified config.
63 Can attach files from 'attachments' list"""
64 try:
65 server = config["hermes"]["mail"]["server"]
66 mailfrom = config["hermes"]["mail"]["from"]
67 mailto = config["hermes"]["mail"]["to"]
69 # Create a text/plain message
70 msg = EmailMessage()
71 msg.set_content(content)
73 msg["Subject"] = subject
74 msg["From"] = mailfrom
75 msg["To"] = mailto
77 for attachment in attachments:
78 msg.add_attachment(
79 attachment.content,
80 maintype=attachment.maintype,
81 subtype=attachment.subtype,
82 filename=attachment.filename,
83 )
85 s = smtplib.SMTP(server)
86 s.send_message(msg)
87 s.quit()
88 except Exception as e:
89 __hermes__.logger.warning(f"Fail to send mail {subject=}: {str(e)}")
91 @staticmethod
92 def sendDiff(
93 config: HermesConfig,
94 contentdesc: str,
95 previous: str,
96 current: str,
97 ):
98 """Send a mail with a diff between two strings.
100 'contentdesc': string (first letter should be lowercase) that will be used
101 in mail subject, and as prefix of mail content
102 'previous': previous data used to compute diff
103 'current': current data used to compute diff
104 """
105 nl = "\n"
107 d = difflib.unified_diff(
108 previous.splitlines(keepends=True),
109 current.splitlines(keepends=True),
110 "previous.txt",
111 "current.txt",
112 n=0,
113 )
114 diff = "".join(d)
116 # Convert string to bytes
117 previous = "".join(previous).encode()
118 current = "".join(current).encode()
119 difffile = diff.encode()
121 if config["hermes"]["mail"]["compress_attachments"]:
122 mimetype = "application/gzip"
123 ext = ".txt.gz"
124 compress = gzip.compress
125 else:
126 mimetype = "text/plain"
127 ext = ".txt"
128 compress = Email._dontCompress # Keep data as is
130 tmpattachments = [
131 Attachment(f"previous{ext}", mimetype, compress(previous)),
132 Attachment(f"current{ext}", mimetype, compress(current)),
133 Attachment(f"diff{ext}", mimetype, compress(difffile)),
134 ]
136 # Ensure attachments doesn't exceed attachment_maxsize
137 attachments = []
138 toobig = []
139 errmsg = ""
140 for a in tmpattachments:
141 if len(a) <= config["hermes"]["mail"]["attachment_maxsize"]:
142 attachments.append(a)
143 else:
144 toobig.append(a.filename)
146 if toobig:
147 errmsg = (
148 f"Some files were too big to be attached to mail: {toobig}.{nl}{nl}"
149 )
151 if len(diff.encode()) < config["hermes"]["mail"]["mailtext_maxsize"]:
152 content = f"{errmsg}{contentdesc.capitalize()}. Diff is:{nl}{nl}{diff}"
153 else:
154 content = (
155 f"{errmsg}{contentdesc.capitalize()}. "
156 "Diff is too big to be displayed in mail content, "
157 "please see attachments or log files."
158 )
160 Email.send(
161 config=config,
162 subject=f"[{config['appname']}] {contentdesc}",
163 content=content,
164 attachments=attachments,
165 )
167 @staticmethod
168 def _dontCompress(data: Any) -> Any:
169 return data