| 97 | ) |
| 98 | |
| 99 | def send( |
| 100 | self, |
| 101 | to: str | list[str], |
| 102 | subject: str, |
| 103 | body: str, |
| 104 | cc: str | list[str] | None = None, |
| 105 | attachs: Sequence[tuple[str, str, IO[Any]]] = (), |
| 106 | mimetype: str = "text/plain", |
| 107 | charset: str | None = None, |
| 108 | _callback: Callable[..., None] | None = None, |
| 109 | ) -> Deferred[None] | None: |
| 110 | from twisted.internet import reactor |
| 111 | |
| 112 | msg: MIMEBase = ( |
| 113 | MIMEMultipart() if attachs else MIMENonMultipart(*mimetype.split("/", 1)) |
| 114 | ) |
| 115 | |
| 116 | to = list(arg_to_iter(to)) |
| 117 | cc = list(arg_to_iter(cc)) |
| 118 | |
| 119 | msg["From"] = self.mailfrom |
| 120 | msg["To"] = COMMASPACE.join(to) |
| 121 | msg["Date"] = formatdate(localtime=True) |
| 122 | msg["Subject"] = subject |
| 123 | rcpts = to[:] |
| 124 | if cc: |
| 125 | rcpts.extend(cc) |
| 126 | msg["Cc"] = COMMASPACE.join(cc) |
| 127 | |
| 128 | if attachs: |
| 129 | if charset: |
| 130 | msg.set_charset(charset) |
| 131 | msg.attach(MIMEText(body, "plain", charset or "us-ascii")) |
| 132 | for attach_name, attach_mimetype, f in attachs: |
| 133 | part = MIMEBase(*attach_mimetype.split("/")) |
| 134 | part.set_payload(f.read()) |
| 135 | Encoders.encode_base64(part) |
| 136 | part.add_header( |
| 137 | "Content-Disposition", "attachment", filename=attach_name |
| 138 | ) |
| 139 | msg.attach(part) |
| 140 | else: |
| 141 | msg.set_payload(body, charset) |
| 142 | |
| 143 | if _callback: |
| 144 | _callback(to=to, subject=subject, body=body, cc=cc, attach=attachs, msg=msg) |
| 145 | |
| 146 | if self.debug: |
| 147 | logger.debug( |
| 148 | "Debug mail sent OK: To=%(mailto)s Cc=%(mailcc)s " |
| 149 | 'Subject="%(mailsubject)s" Attachs=%(mailattachs)d', |
| 150 | { |
| 151 | "mailto": to, |
| 152 | "mailcc": cc, |
| 153 | "mailsubject": subject, |
| 154 | "mailattachs": len(attachs), |
| 155 | }, |
| 156 | ) |