f5ed284f8c 2025-06-21 arcade: use crate::{
f5ed284f8c 2025-06-21 arcade: Cursor,
f5ed284f8c 2025-06-21 arcade: telegram::{
f5ed284f8c 2025-06-21 arcade: encode,
f5ed284f8c 2025-06-21 arcade: TelegramTransport,
f5ed284f8c 2025-06-21 arcade: },
f5ed284f8c 2025-06-21 arcade: utils::{
f5ed284f8c 2025-06-21 arcade: Attachment,
f5ed284f8c 2025-06-21 arcade: RE_DOMAIN,
f5ed284f8c 2025-06-21 arcade: },
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: use std::{
f5ed284f8c 2025-06-21 arcade: borrow::Cow,
f5ed284f8c 2025-06-21 arcade: collections::{
f5ed284f8c 2025-06-21 arcade: HashMap,
f5ed284f8c 2025-06-21 arcade: HashSet,
f5ed284f8c 2025-06-21 arcade: },
f5ed284f8c 2025-06-21 arcade: io::Error,
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: use anyhow::{
f5ed284f8c 2025-06-21 arcade: bail,
f5ed284f8c 2025-06-21 arcade: Context,
f5ed284f8c 2025-06-21 arcade: Result,
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: use async_std::{
f5ed284f8c 2025-06-21 arcade: sync::Arc,
f5ed284f8c 2025-06-21 arcade: task,
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: use mailin_embedded::{
f5ed284f8c 2025-06-21 arcade: Response,
f5ed284f8c 2025-06-21 arcade: response::{
f5ed284f8c 2025-06-21 arcade: INTERNAL_ERROR,
f5ed284f8c 2025-06-21 arcade: INVALID_CREDENTIALS,
f5ed284f8c 2025-06-21 arcade: NO_MAILBOX,
f5ed284f8c 2025-06-21 arcade: OK
f5ed284f8c 2025-06-21 arcade: },
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: use regex::{
f5ed284f8c 2025-06-21 arcade: Regex,
f5ed284f8c 2025-06-21 arcade: escape,
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: use tgbot::types::ChatPeerId;
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: /// `SomeHeaders` object to store data through SMTP session
f5ed284f8c 2025-06-21 arcade: #[derive(Clone, Debug)]
f5ed284f8c 2025-06-21 arcade: struct SomeHeaders {
f5ed284f8c 2025-06-21 arcade: from: String,
f5ed284f8c 2025-06-21 arcade: to: Vec<String>,
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: /// `MailServer` Central object with TG api and configuration
f5ed284f8c 2025-06-21 arcade: #[derive(Clone, Debug)]
f5ed284f8c 2025-06-21 arcade: pub struct MailServer {
f5ed284f8c 2025-06-21 arcade: data: Vec<u8>,
f5ed284f8c 2025-06-21 arcade: headers: Option<SomeHeaders>,
f5ed284f8c 2025-06-21 arcade: relay: bool,
f5ed284f8c 2025-06-21 arcade: tg: Arc<TelegramTransport>,
f5ed284f8c 2025-06-21 arcade: fields: HashSet<String>,
f5ed284f8c 2025-06-21 arcade: address: Regex,
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: impl MailServer {
f5ed284f8c 2025-06-21 arcade: /// Initialize API and read configuration
f5ed284f8c 2025-06-21 arcade: pub fn new(settings: config::Config) -> Result<MailServer> {
f5ed284f8c 2025-06-21 arcade: let api_key = settings.get_string("api_key")
f5ed284f8c 2025-06-21 arcade: .context("[smtp2tg.toml] missing \"api_key\" parameter.\n")?;
f5ed284f8c 2025-06-21 arcade: let mut recipients = HashMap::new();
f5ed284f8c 2025-06-21 arcade: for (name, value) in settings.get_table("recipients")
f5ed284f8c 2025-06-21 arcade: .expect("[smtp2tg.toml] missing table \"recipients\".\n")
f5ed284f8c 2025-06-21 arcade: {
f5ed284f8c 2025-06-21 arcade: let value = value.into_int()
f5ed284f8c 2025-06-21 arcade: .context("[smtp2tg.toml] \"recipient\" table values should be integers.\n")?;
f5ed284f8c 2025-06-21 arcade: recipients.insert(name, value);
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: let default = settings.get_int("default")
f5ed284f8c 2025-06-21 arcade: .context("[smtp2tg.toml] missing \"default\" recipient.\n")?;
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: let tg = Arc::new(TelegramTransport::new(api_key, recipients, default)?);
f5ed284f8c 2025-06-21 arcade: let fields = HashSet::<String>::from_iter(settings.get_array("fields")
f5ed284f8c 2025-06-21 arcade: .expect("[smtp2tg.toml] \"fields\" should be an array")
f5ed284f8c 2025-06-21 arcade: .iter().map(|x| x.clone().into_string().expect("should be strings")));
f5ed284f8c 2025-06-21 arcade: let mut domains: HashSet<String> = HashSet::new();
f5ed284f8c 2025-06-21 arcade: let extra_domains = settings.get_array("domains").unwrap();
f5ed284f8c 2025-06-21 arcade: for domain in extra_domains {
f5ed284f8c 2025-06-21 arcade: let domain = domain.to_string().to_lowercase();
f5ed284f8c 2025-06-21 arcade: if RE_DOMAIN.is_match(&domain) {
f5ed284f8c 2025-06-21 arcade: domains.insert(domain);
f5ed284f8c 2025-06-21 arcade: } else {
f5ed284f8c 2025-06-21 arcade: panic!("[smtp2tg.toml] can't check of domains in \"domains\": {domain}");
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: let domains = domains.into_iter().map(|s| escape(&s))
f5ed284f8c 2025-06-21 arcade: .collect::<Vec<String>>().join("|");
f5ed284f8c 2025-06-21 arcade: let address = Regex::new(&format!("^(?P<user>[a-z0-9][-a-z0-9])(@({domains}))$")).unwrap();
f5ed284f8c 2025-06-21 arcade: let relay = match settings.get_string("unknown")
f5ed284f8c 2025-06-21 arcade: .context("[smtp2tg.toml] can't get \"unknown\" policy.\n")?.as_str()
f5ed284f8c 2025-06-21 arcade: {
f5ed284f8c 2025-06-21 arcade: "relay" => true,
f5ed284f8c 2025-06-21 arcade: "deny" => false,
f5ed284f8c 2025-06-21 arcade: _ => {
f5ed284f8c 2025-06-21 arcade: bail!("[smtp2tg.toml] \"unknown\" should be either \"relay\" or \"deny\".\n");
f5ed284f8c 2025-06-21 arcade: },
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: Ok(MailServer {
f5ed284f8c 2025-06-21 arcade: data: vec!(),
f5ed284f8c 2025-06-21 arcade: headers: None,
f5ed284f8c 2025-06-21 arcade: relay,
f5ed284f8c 2025-06-21 arcade: tg,
f5ed284f8c 2025-06-21 arcade: fields,
f5ed284f8c 2025-06-21 arcade: address,
f5ed284f8c 2025-06-21 arcade: })
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: /// Returns id for provided email address
f5ed284f8c 2025-06-21 arcade: fn get_id (&self, name: &str) -> Result<&ChatPeerId> {
f5ed284f8c 2025-06-21 arcade: // here we need to store String locally to borrow it after
f5ed284f8c 2025-06-21 arcade: let mut link = name;
f5ed284f8c 2025-06-21 arcade: let name: String;
f5ed284f8c 2025-06-21 arcade: if let Some(caps) = self.address.captures(link) {
f5ed284f8c 2025-06-21 arcade: name = caps["name"].to_string();
f5ed284f8c 2025-06-21 arcade: link = &name;
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: match self.tg.get(link) {
f5ed284f8c 2025-06-21 arcade: Ok(addr) => Ok(addr),
f5ed284f8c 2025-06-21 arcade: Err(_) => Ok(&self.tg.default),
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: /// Attempt to deliver one message
f5ed284f8c 2025-06-21 arcade: async fn relay_mail (&self) -> Result<()> {
f5ed284f8c 2025-06-21 arcade: if let Some(headers) = &self.headers {
f5ed284f8c 2025-06-21 arcade: let mail = mail_parser::MessageParser::new().parse(&self.data)
f5ed284f8c 2025-06-21 arcade: .context("Failed to parse mail.")?;
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: // Adding all known addresses to recipient list, for anyone else adding default
f5ed284f8c 2025-06-21 arcade: // Also if list is empty also adding default
f5ed284f8c 2025-06-21 arcade: let mut rcpt: HashSet<&ChatPeerId> = HashSet::new();
f5ed284f8c 2025-06-21 arcade: if headers.to.is_empty() && !self.relay {
f5ed284f8c 2025-06-21 arcade: bail!("Relaying is disabled, and there's no destination address");
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: for item in &headers.to {
f5ed284f8c 2025-06-21 arcade: rcpt.insert(self.get_id(item)?);
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: if rcpt.is_empty() {
f5ed284f8c 2025-06-21 arcade: self.tg.debug("No recipient or envelope address.").await?;
f5ed284f8c 2025-06-21 arcade: rcpt.insert(&self.tg.default);
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: // prepating message header
f5ed284f8c 2025-06-21 arcade: let mut reply: Vec<String> = vec![];
f5ed284f8c 2025-06-21 arcade: if self.fields.contains("subject") {
f5ed284f8c 2025-06-21 arcade: if let Some(subject) = mail.subject() {
f5ed284f8c 2025-06-21 arcade: reply.push(format!("__*Subject:*__ `{}`", encode(subject)));
f5ed284f8c 2025-06-21 arcade: } else if let Some(thread) = mail.thread_name() {
f5ed284f8c 2025-06-21 arcade: reply.push(format!("__*Thread:*__ `{}`", encode(thread)));
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: let mut short_headers: Vec<String> = vec![];
f5ed284f8c 2025-06-21 arcade: // do we need to replace spaces here?
f5ed284f8c 2025-06-21 arcade: if self.fields.contains("from") {
f5ed284f8c 2025-06-21 arcade: short_headers.push(format!("__*From:*__ `{}`", encode(&headers.from)));
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: if self.fields.contains("date") {
f5ed284f8c 2025-06-21 arcade: if let Some(date) = mail.date() {
f5ed284f8c 2025-06-21 arcade: short_headers.push(format!("__*Date:*__ `{date}`"));
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: reply.push(short_headers.join(" "));
f5ed284f8c 2025-06-21 arcade: let header_size = reply.join(" ").len() + 1;
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: let html_parts = mail.html_body_count();
f5ed284f8c 2025-06-21 arcade: let text_parts = mail.text_body_count();
f5ed284f8c 2025-06-21 arcade: let attachments = mail.attachment_count();
f5ed284f8c 2025-06-21 arcade: if html_parts != text_parts {
f5ed284f8c 2025-06-21 arcade: self.tg.debug(&format!("Hm, we have {html_parts} HTML parts and {text_parts} text parts.")).await?;
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: //let mut html_num = 0;
f5ed284f8c 2025-06-21 arcade: let mut text_num = 0;
f5ed284f8c 2025-06-21 arcade: let mut file_num = 0;
f5ed284f8c 2025-06-21 arcade: // let's display first html or text part as body
f5ed284f8c 2025-06-21 arcade: let mut body: Cow<'_, str> = "".into();
f5ed284f8c 2025-06-21 arcade: /*
f5ed284f8c 2025-06-21 arcade: * actually I don't wanna parse that html stuff
f5ed284f8c 2025-06-21 arcade: if html_parts > 0 {
f5ed284f8c 2025-06-21 arcade: let text = mail.body_html(0).unwrap();
f5ed284f8c 2025-06-21 arcade: if text.len() < 4096 - header_size {
f5ed284f8c 2025-06-21 arcade: body = text;
f5ed284f8c 2025-06-21 arcade: html_num = 1;
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: */
f5ed284f8c 2025-06-21 arcade: if body.is_empty() && text_parts > 0 {
f5ed284f8c 2025-06-21 arcade: let text = mail.body_text(0)
f5ed284f8c 2025-06-21 arcade: .context("Failed to extract text from message")?;
f5ed284f8c 2025-06-21 arcade: if text.len() < 4096 - header_size {
f5ed284f8c 2025-06-21 arcade: body = text;
f5ed284f8c 2025-06-21 arcade: text_num = 1;
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: reply.push("```".into());
f5ed284f8c 2025-06-21 arcade: reply.extend(body.lines().map(|x| x.into()));
f5ed284f8c 2025-06-21 arcade: reply.push("```".into());
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: // and let's collect all other attachment parts
f5ed284f8c 2025-06-21 arcade: let mut files_to_send = vec![];
f5ed284f8c 2025-06-21 arcade: /*
f5ed284f8c 2025-06-21 arcade: * let's just skip html parts for now, they just duplicate text?
f5ed284f8c 2025-06-21 arcade: while html_num < html_parts {
f5ed284f8c 2025-06-21 arcade: files_to_send.push(mail.html_part(html_num).unwrap());
f5ed284f8c 2025-06-21 arcade: html_num += 1;
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: */
f5ed284f8c 2025-06-21 arcade: while text_num < text_parts {
f5ed284f8c 2025-06-21 arcade: files_to_send.push(mail.text_part(text_num.try_into()?)
f5ed284f8c 2025-06-21 arcade: .context("Failed to get text part from message.")?);
f5ed284f8c 2025-06-21 arcade: text_num += 1;
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: while file_num < attachments {
f5ed284f8c 2025-06-21 arcade: files_to_send.push(mail.attachment(file_num.try_into()?)
f5ed284f8c 2025-06-21 arcade: .context("Failed to get file part from message.")?);
f5ed284f8c 2025-06-21 arcade: file_num += 1;
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: let msg = reply.join("\n");
f5ed284f8c 2025-06-21 arcade: for chat in rcpt {
f5ed284f8c 2025-06-21 arcade: if !files_to_send.is_empty() {
f5ed284f8c 2025-06-21 arcade: let mut files = vec![];
f5ed284f8c 2025-06-21 arcade: // let mut first_one = true;
f5ed284f8c 2025-06-21 arcade: for chunk in &files_to_send {
f5ed284f8c 2025-06-21 arcade: let data: Vec<u8> = chunk.contents().to_vec();
f5ed284f8c 2025-06-21 arcade: let mut filename: Option<String> = None;
f5ed284f8c 2025-06-21 arcade: for header in chunk.headers() {
f5ed284f8c 2025-06-21 arcade: if header.name() == "Content-Type" {
f5ed284f8c 2025-06-21 arcade: match header.value() {
f5ed284f8c 2025-06-21 arcade: mail_parser::HeaderValue::ContentType(contenttype) => {
f5ed284f8c 2025-06-21 arcade: if let Some(fname) = contenttype.attribute("name") {
f5ed284f8c 2025-06-21 arcade: filename = Some(fname.to_owned());
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: },
f5ed284f8c 2025-06-21 arcade: _ => {
f5ed284f8c 2025-06-21 arcade: self.tg.debug("Attachment has bad ContentType header.").await?;
f5ed284f8c 2025-06-21 arcade: },
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: let filename = if let Some(fname) = filename {
f5ed284f8c 2025-06-21 arcade: fname
f5ed284f8c 2025-06-21 arcade: } else {
f5ed284f8c 2025-06-21 arcade: "Attachment.txt".into()
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: files.push(Attachment {
f5ed284f8c 2025-06-21 arcade: data: Cursor::new(data),
f5ed284f8c 2025-06-21 arcade: name: filename,
f5ed284f8c 2025-06-21 arcade: });
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: self.tg.sendgroup(chat, files, &msg).await?;
f5ed284f8c 2025-06-21 arcade: } else {
f5ed284f8c 2025-06-21 arcade: self.tg.send(chat, &msg).await?;
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: } else {
f5ed284f8c 2025-06-21 arcade: bail!("Required headers were not found.");
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: Ok(())
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: impl mailin_embedded::Handler for MailServer {
f5ed284f8c 2025-06-21 arcade: /// Just deny login auth
f5ed284f8c 2025-06-21 arcade: fn auth_login (&mut self, _username: &str, _password: &str) -> Response {
f5ed284f8c 2025-06-21 arcade: INVALID_CREDENTIALS
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: /// Just deny plain auth
f5ed284f8c 2025-06-21 arcade: fn auth_plain (&mut self, _authorization_id: &str, _authentication_id: &str, _password: &str) -> Response {
f5ed284f8c 2025-06-21 arcade: INVALID_CREDENTIALS
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: /// Verify whether address is deliverable
f5ed284f8c 2025-06-21 arcade: fn rcpt (&mut self, to: &str) -> Response {
f5ed284f8c 2025-06-21 arcade: if self.relay {
f5ed284f8c 2025-06-21 arcade: OK
f5ed284f8c 2025-06-21 arcade: } else {
f5ed284f8c 2025-06-21 arcade: match self.get_id(to) {
f5ed284f8c 2025-06-21 arcade: Ok(_) => OK,
f5ed284f8c 2025-06-21 arcade: Err(_) => {
f5ed284f8c 2025-06-21 arcade: if self.relay {
f5ed284f8c 2025-06-21 arcade: OK
f5ed284f8c 2025-06-21 arcade: } else {
f5ed284f8c 2025-06-21 arcade: NO_MAILBOX
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: /// Save headers we need
f5ed284f8c 2025-06-21 arcade: fn data_start (&mut self, _domain: &str, from: &str, _is8bit: bool, to: &[String]) -> Response {
f5ed284f8c 2025-06-21 arcade: self.headers = Some(SomeHeaders{
f5ed284f8c 2025-06-21 arcade: from: from.to_string(),
f5ed284f8c 2025-06-21 arcade: to: to.to_vec(),
f5ed284f8c 2025-06-21 arcade: });
f5ed284f8c 2025-06-21 arcade: OK
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: /// Save chunk(?) of data
f5ed284f8c 2025-06-21 arcade: fn data (&mut self, buf: &[u8]) -> Result<(), Error> {
f5ed284f8c 2025-06-21 arcade: self.data.append(buf.to_vec().as_mut());
f5ed284f8c 2025-06-21 arcade: Ok(())
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade:
f5ed284f8c 2025-06-21 arcade: /// Attempt to send email, return temporary error if that fails
f5ed284f8c 2025-06-21 arcade: fn data_end (&mut self) -> Response {
f5ed284f8c 2025-06-21 arcade: let mut result = OK;
f5ed284f8c 2025-06-21 arcade: task::block_on(async {
f5ed284f8c 2025-06-21 arcade: // relay mail
f5ed284f8c 2025-06-21 arcade: if let Err(err) = self.relay_mail().await {
f5ed284f8c 2025-06-21 arcade: result = INTERNAL_ERROR;
f5ed284f8c 2025-06-21 arcade: // in case that fails - inform default recipient
f5ed284f8c 2025-06-21 arcade: if let Err(err) = self.tg.debug(&format!("Sending emails failed:\n{err:?}")).await {
f5ed284f8c 2025-06-21 arcade: // in case that also fails - write some logs and bail
f5ed284f8c 2025-06-21 arcade: eprintln!("{err:?}");
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: };
f5ed284f8c 2025-06-21 arcade: });
f5ed284f8c 2025-06-21 arcade: // clear - just in case
f5ed284f8c 2025-06-21 arcade: self.data = vec![];
f5ed284f8c 2025-06-21 arcade: self.headers = None;
f5ed284f8c 2025-06-21 arcade: result
f5ed284f8c 2025-06-21 arcade: }
f5ed284f8c 2025-06-21 arcade: }