Annotation For src/mail.rs
Logged in as anonymous

Origin for each line in src/mail.rs from check-in 78b11b0319:

78b11b0319 2026-08-01    1: //! SMTP server implementation for receiving and processing emails.
78b11b0319 2026-08-01    2: //!
78b11b0319 2026-08-01    3: //! This module handles SMTP connections, email parsing, and forwarding to
78b11b0319 2026-08-01    4: //! Telegram.
78b11b0319 2026-08-01    5: 
f5ed284f8c 2025-06-21    6: use crate::{
f5ed284f8c 2025-06-21    7: 	Cursor,
d87e80b9be 2026-01-12    8: 	telegram::TelegramTransport,
f5ed284f8c 2025-06-21    9: 	utils::{
f5ed284f8c 2025-06-21   10: 		Attachment,
f5ed284f8c 2025-06-21   11: 		RE_DOMAIN,
d87e80b9be 2026-01-12   12: 		validate,
f5ed284f8c 2025-06-21   13: 	},
f5ed284f8c 2025-06-21   14: };
f5ed284f8c 2025-06-21   15: 
f5ed284f8c 2025-06-21   16: use std::{
f5ed284f8c 2025-06-21   17: 	collections::{
f5ed284f8c 2025-06-21   18: 		HashMap,
f5ed284f8c 2025-06-21   19: 		HashSet,
f5ed284f8c 2025-06-21   20: 	},
f5ed284f8c 2025-06-21   21: 	io::Error,
072229b5bf 2026-01-01   22: 	sync::Arc,
f5ed284f8c 2025-06-21   23: };
f5ed284f8c 2025-06-21   24: 
14ef340959 2026-01-01   25: use async_compat::Compat;
f5ed284f8c 2025-06-21   26: use mailin_embedded::{
f5ed284f8c 2025-06-21   27: 	Response,
f5ed284f8c 2025-06-21   28: 	response::{
f5ed284f8c 2025-06-21   29: 		INTERNAL_ERROR,
f5ed284f8c 2025-06-21   30: 		INVALID_CREDENTIALS,
f5ed284f8c 2025-06-21   31: 		NO_MAILBOX,
f5ed284f8c 2025-06-21   32: 		OK
f5ed284f8c 2025-06-21   33: 	},
f5ed284f8c 2025-06-21   34: };
f5ed284f8c 2025-06-21   35: use regex::{
f5ed284f8c 2025-06-21   36: 	Regex,
78b11b0319 2026-08-01   37: 	RegexBuilder,
f5ed284f8c 2025-06-21   38: 	escape,
a044f68fa7 2025-08-23   39: };
a044f68fa7 2025-08-23   40: use stacked_errors::{
a044f68fa7 2025-08-23   41: 	Result,
a044f68fa7 2025-08-23   42: 	StackableErr,
a044f68fa7 2025-08-23   43: 	bail,
f5ed284f8c 2025-06-21   44: };
f5ed284f8c 2025-06-21   45: use tgbot::types::ChatPeerId;
f5ed284f8c 2025-06-21   46: 
f5ed284f8c 2025-06-21   47: /// `SomeHeaders` object to store data through SMTP session
f5ed284f8c 2025-06-21   48: #[derive(Clone, Debug)]
f5ed284f8c 2025-06-21   49: struct SomeHeaders {
f5ed284f8c 2025-06-21   50: 	from: String,
f5ed284f8c 2025-06-21   51: 	to: Vec<String>,
f5ed284f8c 2025-06-21   52: }
f5ed284f8c 2025-06-21   53: 
f5ed284f8c 2025-06-21   54: /// `MailServer` Central object with TG api and configuration
f5ed284f8c 2025-06-21   55: #[derive(Clone, Debug)]
f5ed284f8c 2025-06-21   56: pub struct MailServer {
f5ed284f8c 2025-06-21   57: 	data: Vec<u8>,
f5ed284f8c 2025-06-21   58: 	headers: Option<SomeHeaders>,
f5ed284f8c 2025-06-21   59: 	tg: Arc<TelegramTransport>,
f5ed284f8c 2025-06-21   60: 	fields: HashSet<String>,
f5ed284f8c 2025-06-21   61: 	address: Regex,
f5ed284f8c 2025-06-21   62: }
f5ed284f8c 2025-06-21   63: 
f5ed284f8c 2025-06-21   64: impl MailServer {
78b11b0319 2026-08-01   65: 	/// Initializes the mail server: sets up the Telegram API client and
78b11b0319 2026-08-01   66: 	/// validates all required configuration values.
78b11b0319 2026-08-01   67: 	///
78b11b0319 2026-08-01   68: 	/// # Arguments
78b11b0319 2026-08-01   69: 	/// * `settings` - Parsed application configuration.
78b11b0319 2026-08-01   70: 	///
78b11b0319 2026-08-01   71: 	/// # Errors
78b11b0319 2026-08-01   72: 	/// Returns an error if required configuration values are missing or invalid.
78b11b0319 2026-08-01   73: 	/// server fails to start.
78b11b0319 2026-08-01   74: 	pub fn new (settings: config::Config) -> Result<MailServer> {
f5ed284f8c 2025-06-21   75: 		let api_key = settings.get_string("api_key")
f5ed284f8c 2025-06-21   76: 			.context("[smtp2tg.toml] missing \"api_key\" parameter.\n")?;
f5ed284f8c 2025-06-21   77: 		let mut recipients = HashMap::new();
f5ed284f8c 2025-06-21   78: 		for (name, value) in settings.get_table("recipients")
78b11b0319 2026-08-01   79: 			.context("[smtp2tg.toml] missing table \"recipients\".\n")?
f5ed284f8c 2025-06-21   80: 		{
f5ed284f8c 2025-06-21   81: 			let value = value.into_int()
f5ed284f8c 2025-06-21   82: 				.context("[smtp2tg.toml] \"recipient\" table values should be integers.\n")?;
78b11b0319 2026-08-01   83: 			recipients.insert(name.to_lowercase(), value);
f5ed284f8c 2025-06-21   84: 		}
f5ed284f8c 2025-06-21   85: 
14ef340959 2026-01-01   86: 		let tg = Arc::new(TelegramTransport::new(api_key, recipients, &settings)?);
f5ed284f8c 2025-06-21   87: 		let fields = HashSet::<String>::from_iter(settings.get_array("fields")
78b11b0319 2026-08-01   88: 			.context("[smtp2tg.toml] \"fields\" should be an array")?
78b11b0319 2026-08-01   89: 			.iter().map(|x| x.clone().into_string().context("should be strings"))
78b11b0319 2026-08-01   90: 			.collect::<Result<Vec<String>>>()?);
f5ed284f8c 2025-06-21   91: 		let mut domains: HashSet<String> = HashSet::new();
a044f68fa7 2025-08-23   92: 		let extra_domains = settings.get_array("domains").stack()?;
f5ed284f8c 2025-06-21   93: 		for domain in extra_domains {
f5ed284f8c 2025-06-21   94: 			let domain = domain.to_string().to_lowercase();
f5ed284f8c 2025-06-21   95: 			if RE_DOMAIN.is_match(&domain) {
f5ed284f8c 2025-06-21   96: 				domains.insert(domain);
f5ed284f8c 2025-06-21   97: 			} else {
78b11b0319 2026-08-01   98: 				bail!("[smtp2tg.toml] can't check domains in \"domains\": {domain}");
f5ed284f8c 2025-06-21   99: 			}
78b11b0319 2026-08-01  100: 		}
78b11b0319 2026-08-01  101: 		if domains.is_empty() {
78b11b0319 2026-08-01  102: 			bail!("No domains, need at least one: default `localhost` would do.");
f5ed284f8c 2025-06-21  103: 		}
f5ed284f8c 2025-06-21  104: 		let domains = domains.into_iter().map(|s| escape(&s))
f5ed284f8c 2025-06-21  105: 			.collect::<Vec<String>>().join("|");
78b11b0319 2026-08-01  106: 		let address = RegexBuilder::new(&format!("^[a-z0-9][a-z0-9.-]*(@({domains}))?$"))
78b11b0319 2026-08-01  107: 			.case_insensitive(true).build().stack()?;
f5ed284f8c 2025-06-21  108: 
f5ed284f8c 2025-06-21  109: 		Ok(MailServer {
f5ed284f8c 2025-06-21  110: 			data: vec!(),
f5ed284f8c 2025-06-21  111: 			headers: None,
f5ed284f8c 2025-06-21  112: 			tg,
f5ed284f8c 2025-06-21  113: 			fields,
f5ed284f8c 2025-06-21  114: 			address,
f5ed284f8c 2025-06-21  115: 		})
f5ed284f8c 2025-06-21  116: 	}
f5ed284f8c 2025-06-21  117: 
78b11b0319 2026-08-01  118: 	/// Retrieves the Telegram chat ID for a given email address, checks that
78b11b0319 2026-08-01  119: 	/// used domain is allowed.
78b11b0319 2026-08-01  120: 	///
78b11b0319 2026-08-01  121: 	/// # Arguments
78b11b0319 2026-08-01  122: 	/// * `name` - Email address or username to look up.
78b11b0319 2026-08-01  123: 	///
78b11b0319 2026-08-01  124: 	/// # Returns
78b11b0319 2026-08-01  125: 	/// * `Result<ChatPeerId>` - Telegram chat ID for the address, or default if
78b11b0319 2026-08-01  126: 	///   not found.
78b11b0319 2026-08-01  127: 	pub fn get_id (&self, name: &str) -> Result<&ChatPeerId> {
78b11b0319 2026-08-01  128: 		if self.address.is_match(name) {
78b11b0319 2026-08-01  129: 			Ok(self.tg.get(name).unwrap_or(&self.tg.default))
78b11b0319 2026-08-01  130: 		} else {
78b11b0319 2026-08-01  131: 			bail!("Doesn't look like address from one of our domains.");
f5ed284f8c 2025-06-21  132: 		}
f5ed284f8c 2025-06-21  133: 	}
f5ed284f8c 2025-06-21  134: 
f5ed284f8c 2025-06-21  135: 	/// Attempt to deliver one message
f5ed284f8c 2025-06-21  136: 	async fn relay_mail (&self) -> Result<()> {
f5ed284f8c 2025-06-21  137: 		if let Some(headers) = &self.headers {
f5ed284f8c 2025-06-21  138: 			let mail = mail_parser::MessageParser::new().parse(&self.data)
f5ed284f8c 2025-06-21  139: 				.context("Failed to parse mail.")?;
f5ed284f8c 2025-06-21  140: 
f5ed284f8c 2025-06-21  141: 			// Adding all known addresses to recipient list, for anyone else adding default
f5ed284f8c 2025-06-21  142: 			// Also if list is empty also adding default
f5ed284f8c 2025-06-21  143: 			let mut rcpt: HashSet<&ChatPeerId> = HashSet::new();
78b11b0319 2026-08-01  144: 			if headers.to.is_empty() {
f5ed284f8c 2025-06-21  145: 				bail!("Relaying is disabled, and there's no destination address");
f5ed284f8c 2025-06-21  146: 			}
f5ed284f8c 2025-06-21  147: 			for item in &headers.to {
f5ed284f8c 2025-06-21  148: 				rcpt.insert(self.get_id(item)?);
f5ed284f8c 2025-06-21  149: 			};
f5ed284f8c 2025-06-21  150: 			if rcpt.is_empty() {
f5ed284f8c 2025-06-21  151: 				self.tg.debug("No recipient or envelope address.").await?;
f5ed284f8c 2025-06-21  152: 				rcpt.insert(&self.tg.default);
f5ed284f8c 2025-06-21  153: 			};
f5ed284f8c 2025-06-21  154: 
d87e80b9be 2026-01-12  155: 			// preparing message header
d87e80b9be 2026-01-12  156: 			let mut reply: Vec<String> = vec!["<blockquote expandable>".into()];
14ef340959 2026-01-01  157: 			if self.fields.contains("subject") {
14ef340959 2026-01-01  158: 				if let Some(subject) = mail.subject() {
d87e80b9be 2026-01-12  159: 					reply.push(format!("<u><i>Subject:</i></u> <code>{}</code>", validate(subject).stack()?));
14ef340959 2026-01-01  160: 				} else if let Some(thread) = mail.thread_name() {
d87e80b9be 2026-01-12  161: 					reply.push(format!("<u><i>Thread:</i></u> <code>{}</code>", validate(thread).stack()?));
14ef340959 2026-01-01  162: 				}
14ef340959 2026-01-01  163: 			}
14ef340959 2026-01-01  164: 			// do we need to replace spaces here?
14ef340959 2026-01-01  165: 			if self.fields.contains("from") {
d87e80b9be 2026-01-12  166: 				reply.push(format!("<u><i>From:</i></u> <code>{}</code>", validate(&headers.from).stack()?));
d87e80b9be 2026-01-12  167: 			}
d87e80b9be 2026-01-12  168: 			if self.fields.contains("date")
d87e80b9be 2026-01-12  169: 				&& let Some(date) = mail.date()
d87e80b9be 2026-01-12  170: 			{
d87e80b9be 2026-01-12  171: 				reply.push(format!("<u><i>Date:</i></u> <code>{date}</code>"));
d87e80b9be 2026-01-12  172: 			}
d87e80b9be 2026-01-12  173: 			reply.push("</blockquote><pre>".into());
d87e80b9be 2026-01-12  174: 			let reply = reply.join("\n");
f5ed284f8c 2025-06-21  175: 
f5ed284f8c 2025-06-21  176: 			let html_parts = mail.html_body_count();
f5ed284f8c 2025-06-21  177: 			let text_parts = mail.text_body_count();
f5ed284f8c 2025-06-21  178: 			let attachments = mail.attachment_count();
f5ed284f8c 2025-06-21  179: 			if html_parts != text_parts {
f5ed284f8c 2025-06-21  180: 				self.tg.debug(&format!("Hm, we have {html_parts} HTML parts and {text_parts} text parts.")).await?;
f5ed284f8c 2025-06-21  181: 			}
f5ed284f8c 2025-06-21  182: 			//let mut html_num = 0;
f5ed284f8c 2025-06-21  183: 			let mut text_num = 0;
f5ed284f8c 2025-06-21  184: 			let mut file_num = 0;
f5ed284f8c 2025-06-21  185: 			// let's display first html or text part as body
d87e80b9be 2026-01-12  186: 			let mut body: String = "".into();
f5ed284f8c 2025-06-21  187: 			/*
f5ed284f8c 2025-06-21  188: 			 * actually I don't wanna parse that html stuff
f5ed284f8c 2025-06-21  189: 			if html_parts > 0 {
a044f68fa7 2025-08-23  190: 				let text = mail.body_html(0).stack()?;
f5ed284f8c 2025-06-21  191: 				if text.len() < 4096 - header_size {
f5ed284f8c 2025-06-21  192: 					body = text;
f5ed284f8c 2025-06-21  193: 					html_num = 1;
f5ed284f8c 2025-06-21  194: 				}
f5ed284f8c 2025-06-21  195: 			};
f5ed284f8c 2025-06-21  196: 			*/
f5ed284f8c 2025-06-21  197: 			if body.is_empty() && text_parts > 0 {
f5ed284f8c 2025-06-21  198: 				let text = mail.body_text(0)
d87e80b9be 2026-01-12  199: 					.context("Failed to extract text from message")?
d87e80b9be 2026-01-12  200: 					.replace("\r\n", "\n");
1c10bccb4e 2026-01-18  201: 				let text = validate(&text).stack()?;
d87e80b9be 2026-01-12  202: 				// 6:
d87e80b9be 2026-01-12  203: 				// - (headers)
d87e80b9be 2026-01-12  204: 				// - (mail text)
d87e80b9be 2026-01-12  205: 				// - 6: </pre>
d87e80b9be 2026-01-12  206: 				if text.len() < 4096 - ( reply.len() + 6 ) {
1c10bccb4e 2026-01-18  207: 					body = text.to_string();
f5ed284f8c 2025-06-21  208: 					text_num = 1;
f5ed284f8c 2025-06-21  209: 				}
f5ed284f8c 2025-06-21  210: 			};
1c10bccb4e 2026-01-18  211: 			let msg = format!("{}{}</pre>", reply, body);
f5ed284f8c 2025-06-21  212: 
f5ed284f8c 2025-06-21  213: 			// and let's collect all other attachment parts
f5ed284f8c 2025-06-21  214: 			let mut files_to_send = vec![];
f5ed284f8c 2025-06-21  215: 			/*
f5ed284f8c 2025-06-21  216: 			 * let's just skip html parts for now, they just duplicate text?
f5ed284f8c 2025-06-21  217: 			while html_num < html_parts {
a044f68fa7 2025-08-23  218: 				files_to_send.push(mail.html_part(html_num).stack()?);
f5ed284f8c 2025-06-21  219: 				html_num += 1;
f5ed284f8c 2025-06-21  220: 			}
f5ed284f8c 2025-06-21  221: 			*/
f5ed284f8c 2025-06-21  222: 			while text_num < text_parts {
a044f68fa7 2025-08-23  223: 				files_to_send.push(mail.text_part(text_num.try_into().stack()?)
f5ed284f8c 2025-06-21  224: 					.context("Failed to get text part from message.")?);
f5ed284f8c 2025-06-21  225: 				text_num += 1;
f5ed284f8c 2025-06-21  226: 			}
f5ed284f8c 2025-06-21  227: 			while file_num < attachments {
a044f68fa7 2025-08-23  228: 				files_to_send.push(mail.attachment(file_num.try_into().stack()?)
f5ed284f8c 2025-06-21  229: 					.context("Failed to get file part from message.")?);
f5ed284f8c 2025-06-21  230: 				file_num += 1;
f5ed284f8c 2025-06-21  231: 			}
f5ed284f8c 2025-06-21  232: 
f5ed284f8c 2025-06-21  233: 			for chat in rcpt {
f5ed284f8c 2025-06-21  234: 				if !files_to_send.is_empty() {
f5ed284f8c 2025-06-21  235: 					let mut files = vec![];
f5ed284f8c 2025-06-21  236: 					// let mut first_one = true;
f5ed284f8c 2025-06-21  237: 					for chunk in &files_to_send {
f5ed284f8c 2025-06-21  238: 						let data: Vec<u8> = chunk.contents().to_vec();
f5ed284f8c 2025-06-21  239: 						let mut filename: Option<String> = None;
f5ed284f8c 2025-06-21  240: 						for header in chunk.headers() {
f5ed284f8c 2025-06-21  241: 							if header.name() == "Content-Type" {
f5ed284f8c 2025-06-21  242: 								match header.value() {
f5ed284f8c 2025-06-21  243: 									mail_parser::HeaderValue::ContentType(contenttype) => {
f5ed284f8c 2025-06-21  244: 										if let Some(fname) = contenttype.attribute("name") {
f5ed284f8c 2025-06-21  245: 											filename = Some(fname.to_owned());
f5ed284f8c 2025-06-21  246: 										}
f5ed284f8c 2025-06-21  247: 									},
f5ed284f8c 2025-06-21  248: 									_ => {
f5ed284f8c 2025-06-21  249: 										self.tg.debug("Attachment has bad ContentType header.").await?;
f5ed284f8c 2025-06-21  250: 									},
f5ed284f8c 2025-06-21  251: 								};
f5ed284f8c 2025-06-21  252: 							};
f5ed284f8c 2025-06-21  253: 						};
f5ed284f8c 2025-06-21  254: 						let filename = if let Some(fname) = filename {
f5ed284f8c 2025-06-21  255: 							fname
f5ed284f8c 2025-06-21  256: 						} else {
f5ed284f8c 2025-06-21  257: 							"Attachment.txt".into()
f5ed284f8c 2025-06-21  258: 						};
f5ed284f8c 2025-06-21  259: 						files.push(Attachment {
f5ed284f8c 2025-06-21  260: 							data: Cursor::new(data),
f5ed284f8c 2025-06-21  261: 							name: filename,
f5ed284f8c 2025-06-21  262: 						});
f5ed284f8c 2025-06-21  263: 					}
f5ed284f8c 2025-06-21  264: 					self.tg.sendgroup(chat, files, &msg).await?;
f5ed284f8c 2025-06-21  265: 				} else {
f5ed284f8c 2025-06-21  266: 					self.tg.send(chat, &msg).await?;
f5ed284f8c 2025-06-21  267: 				}
f5ed284f8c 2025-06-21  268: 			}
f5ed284f8c 2025-06-21  269: 		} else {
f5ed284f8c 2025-06-21  270: 			bail!("Required headers were not found.");
f5ed284f8c 2025-06-21  271: 		}
f5ed284f8c 2025-06-21  272: 		Ok(())
f5ed284f8c 2025-06-21  273: 	}
f5ed284f8c 2025-06-21  274: }
f5ed284f8c 2025-06-21  275: 
78b11b0319 2026-08-01  276: /// SMTP handler implementation for mailin-embedded.
f5ed284f8c 2025-06-21  277: impl mailin_embedded::Handler for MailServer {
f5ed284f8c 2025-06-21  278: 	/// Just deny login auth
f5ed284f8c 2025-06-21  279: 	fn auth_login (&mut self, _username: &str, _password: &str) -> Response {
f5ed284f8c 2025-06-21  280: 		INVALID_CREDENTIALS
f5ed284f8c 2025-06-21  281: 	}
f5ed284f8c 2025-06-21  282: 
f5ed284f8c 2025-06-21  283: 	/// Just deny plain auth
f5ed284f8c 2025-06-21  284: 	fn auth_plain (&mut self, _authorization_id: &str, _authentication_id: &str, _password: &str) -> Response {
f5ed284f8c 2025-06-21  285: 		INVALID_CREDENTIALS
f5ed284f8c 2025-06-21  286: 	}
f5ed284f8c 2025-06-21  287: 
f5ed284f8c 2025-06-21  288: 	/// Verify whether address is deliverable
f5ed284f8c 2025-06-21  289: 	fn rcpt (&mut self, to: &str) -> Response {
78b11b0319 2026-08-01  290: 		if self.get_id(to).is_ok() {
f5ed284f8c 2025-06-21  291: 			OK
f5ed284f8c 2025-06-21  292: 		} else {
78b11b0319 2026-08-01  293: 			NO_MAILBOX
f5ed284f8c 2025-06-21  294: 		}
f5ed284f8c 2025-06-21  295: 	}
f5ed284f8c 2025-06-21  296: 
f5ed284f8c 2025-06-21  297: 	/// Save headers we need
f5ed284f8c 2025-06-21  298: 	fn data_start (&mut self, _domain: &str, from: &str, _is8bit: bool, to: &[String]) -> Response {
f5ed284f8c 2025-06-21  299: 		self.headers = Some(SomeHeaders{
f5ed284f8c 2025-06-21  300: 			from: from.to_string(),
f5ed284f8c 2025-06-21  301: 			to: to.to_vec(),
f5ed284f8c 2025-06-21  302: 		});
f5ed284f8c 2025-06-21  303: 		OK
f5ed284f8c 2025-06-21  304: 	}
f5ed284f8c 2025-06-21  305: 
f5ed284f8c 2025-06-21  306: 	/// Save chunk(?) of data
a044f68fa7 2025-08-23  307: 	fn data (&mut self, buf: &[u8]) -> std::result::Result<(), Error> {
f5ed284f8c 2025-06-21  308: 		self.data.append(buf.to_vec().as_mut());
f5ed284f8c 2025-06-21  309: 		Ok(())
f5ed284f8c 2025-06-21  310: 	}
f5ed284f8c 2025-06-21  311: 
f5ed284f8c 2025-06-21  312: 	/// Attempt to send email, return temporary error if that fails
f5ed284f8c 2025-06-21  313: 	fn data_end (&mut self) -> Response {
f5ed284f8c 2025-06-21  314: 		let mut result = OK;
14ef340959 2026-01-01  315: 		smol::block_on(Compat::new(async {
f5ed284f8c 2025-06-21  316: 			// relay mail
f5ed284f8c 2025-06-21  317: 			if let Err(err) = self.relay_mail().await {
f5ed284f8c 2025-06-21  318: 				result = INTERNAL_ERROR;
f5ed284f8c 2025-06-21  319: 				// in case that fails - inform default recipient
1c10bccb4e 2026-01-18  320: 				if let Err(err) = self.tg.debug(&format!("Sending emails failed:\n{err:}")).await {
f5ed284f8c 2025-06-21  321: 					// in case that also fails - write some logs and bail
f5ed284f8c 2025-06-21  322: 					eprintln!("{err:?}");
f5ed284f8c 2025-06-21  323: 				};
f5ed284f8c 2025-06-21  324: 			};
14ef340959 2026-01-01  325: 		}));
f5ed284f8c 2025-06-21  326: 		// clear - just in case
f5ed284f8c 2025-06-21  327: 		self.data = vec![];
f5ed284f8c 2025-06-21  328: 		self.headers = None;
f5ed284f8c 2025-06-21  329: 		result
f5ed284f8c 2025-06-21  330: 	}
f5ed284f8c 2025-06-21  331: }