Coverage for jaypore_ci/remotes/email.py: 97%
62 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-30 09:04 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-30 09:04 +0000
1"""
2An email remote.
4This is used to report pipeline status via email.
5Multiple updates appear as a single thread.
6"""
7import os
8import time
9import smtplib
10from html import escape as html_escape
12from email.headerregistry import Address
13from email.message import EmailMessage
14from pathlib import Path
15from urllib.parse import urlparse
18from jaypore_ci.interfaces import Remote, Repo
19from jaypore_ci.logging import logger
22class Email(Remote): # pylint: disable=too-many-instance-attributes
23 """
24 You can send pipeline status via email using this remote. In order to use it you
25 can specify the following environment variables in your secrets:
27 .. code-block:: console
29 JAYPORE_EMAIL_ADDR=email-account@gmail.com
30 JAYPORE_EMAIL_PASSWORD=some-app-password
31 JAYPORE_EMAIL_TO=myself@gmail.com,mailing-list@gmail.com
32 JAYPORE_EMAIL_FROM=noreply@gmail.com
34 If you're using something other than gmail, you can specify
35 `JAYPORE_EMAIL_HOST` and `JAYPORE_EMAIL_PORT` as well.
37 Once that is done you can supply this remote to your pipeline instead of
38 the usual gitea one.
40 .. code-block:: python
42 from jaypore_ci import jci, remotes, repos
44 git = repos.Git.from_env()
45 email = remotes.Email.from_env(repo=git)
46 with jci.Pipeline(repo=git, remote=email) as p:
47 pass
48 # Do something
50 :param host: What smtp host to use.
51 :param port: Smtp port to use.
52 :param addr: Smtp address to use for login.
53 :param password: Smtp password to use for login.
54 :param email_to: Which address the email should go to.
55 :param email_from: Which address should be the sender of this email.
56 :param subject: The subject line of the email.
57 :param only_on_failure: If set to True, a single email will be sent when
58 the pipeline fails. In all other cases no email is
59 sent.
60 :param publish_interval: Determines the delay in sending another email when
61 we are sending multiple email updates in a single
62 email thread. If `only_on_failure` is set, this
63 option is ignored.
64 """
66 @classmethod
67 def from_env(cls, *, repo: Repo) -> "Email":
68 """
69 Creates a remote instance from the environment.
70 """
71 remote = urlparse(repo.remote)
72 owner = Path(remote.path).parts[1]
73 name = Path(remote.path).parts[2].replace(".git", "")
74 return cls(
75 host=os.environ.get("JAYPORE_EMAIL_HOST", "smtp.gmail.com"),
76 port=int(os.environ.get("JAYPORE_EMAIL_PORT", 465)),
77 addr=os.environ["JAYPORE_EMAIL_ADDR"],
78 password=os.environ["JAYPORE_EMAIL_PASSWORD"],
79 email_to=os.environ["JAYPORE_EMAIL_TO"],
80 email_from=os.environ.get(
81 "JAYPORE_EMAIL_FROM", os.environ["JAYPORE_EMAIL_ADDR"]
82 ),
83 subject=f"JCI [{owner}/{name}] [{repo.branch} {repo.sha[:8]}]",
84 branch=repo.branch,
85 sha=repo.sha,
86 )
88 def __init__(
89 self,
90 *,
91 host: str,
92 port: int,
93 addr: str,
94 password: str,
95 email_to: str,
96 email_from: str,
97 subject: str,
98 only_on_failure: bool = False,
99 publish_interval: int = 30,
100 **kwargs,
101 ): # pylint: disable=too-many-arguments
102 super().__init__(**kwargs)
103 # --- customer
104 self.host = host
105 self.port = port
106 self.addr = addr
107 self.password = password
108 self.email_to = email_to
109 self.email_from = email_from
110 self.subject = subject
111 self.timeout = 10
112 self.publish_interval = publish_interval
113 self.only_on_failure = only_on_failure
114 # ---
115 self.__smtp__ = None
116 self.__last_published_at__ = None
117 self.__last_report__ = None
119 @property
120 def smtp(self):
121 if self.__smtp__ is None:
122 smtp = smtplib.SMTP_SSL(self.host, self.port)
123 smtp.ehlo()
124 smtp.login(self.addr, self.password)
125 self.__smtp__ = smtp
126 return self.__smtp__
128 def logging(self):
129 """
130 Return's a logging instance with information about gitea bound to it.
131 """
132 return logger.bind(addr=self.addr, host=self.host, port=self.port)
134 def publish(self, report: str, status: str) -> None:
135 """
136 Will publish the report via email.
138 :param report: Report to write to remote.
139 :param status: One of ["pending", "success", "error", "failure",
140 "warning"] This is the dot next to each commit in gitea.
141 """
142 assert status in ("pending", "success", "error", "failure", "warning")
143 if (
144 self.__last_published_at__ is not None
145 and (time.time() - self.__last_published_at__) < self.publish_interval
146 and status not in ("success", "failure")
147 ) or (self.only_on_failure and status != "failure"):
148 return
149 if self.__last_report__ == report:
150 return
151 self.__last_report__ = report
152 self.__last_published_at__ = time.time()
153 # Let's send the email
154 msg = EmailMessage()
155 msg["Subject"] = self.subject
156 msg["From"] = Address("JayporeCI", "JayporeCI", self.email_from)
157 msg["To"] = self.email_to
158 msg.set_content(report)
159 msg.add_alternative(
160 f"<html><body><pre>{html_escape(report)}</pre></body></html>",
161 subtype="html",
162 )
163 try:
164 self.smtp.send_message(msg)
165 except Exception as e: # pylint: disable=broad-except
166 self.logging().exception(e)
167 self.__last_published_at__ = time.time()
168 self.logging().info(
169 "Report published",
170 subject=self.subject,
171 email_from=self.email_from,
172 email_to=self.email_to,
173 )