From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id B80161FF13E for ; Fri, 06 Mar 2026 09:21:40 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3A0091A705; Fri, 6 Mar 2026 09:22:47 +0100 (CET) Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 Date: Fri, 06 Mar 2026 09:22:07 +0100 Message-Id: To: "Shannon Sterz" Subject: Re: [PATCH proxmox v3] sendmail: conform more to mime Content-Disposition header and wrap lines X-Mailer: aerc 0.20.0 References: <20260225112107.99905-1-s.sterz@proxmox.com> In-Reply-To: <20260225112107.99905-1-s.sterz@proxmox.com> From: "Shannon Sterz" X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1772785299689 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.095 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_MSPIKE_H2 0.001 Average reputation (+2) SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 6YWVJNF4FXER5LQ73KFKOW6WCV4AVB64 X-Message-ID-Hash: 6YWVJNF4FXER5LQ73KFKOW6WCV4AVB64 X-MailFrom: s.sterz@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: pbs-devel@lists.proxmox.com X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: ping, since this was reviewed and is currently holding up live peat from sending out mails, it'd be nice if this could be applied. if anyone wants to do additional testing, but is unsure how, this patch is in use on test peat, so anyone that has access there can try it out. alternatively, you can ping me if you don't have access. thanks! On Wed Feb 25, 2026 at 12:21 PM CET, Shannon Sterz wrote: > the http and mime Content-Disposition headers function slightly > differently. for the http version the following would be valid > according to rfc 6266 [1]: > > Content-Disposition: attachment; filename=3D"file.pdf"; > filename*=3DUTF-8''file.pdf > > specifying multiple filename attributes like this isn't valid for mime > messages. according to rfc 2183 [2] the above should be expressed like > so for mime messages (one variant, multiple are supported): > > Content-Disposition: attachment; > filename*0*=3DUTF-8''file.bin > > while rfc 2183 [2] would in theory allow for multiple filename > parameters similar to rfc 6266, in practice MIME::Tools [2] considers > this as ambigous content [3]. in more recent versions of Proxmox Mail > Gateway, such messages are rejected [4]. > > this patch implements proper filename handling and also wraps > filenames in the content disposition and content type headers > appropriatelly to a mail being rejected due to having mime headers > that are too long as described in rfcs 2231 [5] and 2045 [6]. > > [1]: https://datatracker.ietf.org/doc/html/rfc6266/ > [2]: https://datatracker.ietf.org/doc/html/rfc2183#section-2 > [3]: https://metacpan.org/pod/MIME::Head#ambiguous_content > [4]: > https://git.proxmox.com/?p=3Dpmg-api.git;a=3Dcommitdiff;h=3D66f51c62f789b= 4c20308b7594fbbb357721b0844 > [5]: https://datatracker.ietf.org/doc/html/rfc2231#section-7 > [6]: https://datatracker.ietf.org/doc/html/rfc2045/#section-6.8 > > Signed-off-by: Shannon Sterz > Reviewed-by: Lukas Wagner > --- > > > tested this with peat on the following muas: > > * aerc > * thunderbird > * ios mail app > * canary mail app > * start mail web client > * gmail web client > * outlook web client > > noticed this when peat suddenly struggled to send mails. thanks @ Stoiko > Ivanov for helping with analysing this issue. > > Changelog: > --------- > > changes since v2 (thanks @ Lukas Wagner): > > * add more documentation on how the new formatting helper works. > * add test cases for the formatting helper itself. > * fixed a bug when formatting text that wouldn't exceed a line but > should have its suffix skipped in the last line, would not have > skipped the suffix. > > changes since v1: > > * wrap lines more correctly ensuring that encoded lines don't exceed 76 > chars (thanks @ Stoiko Ivanov) > * refactored the wrapping logic to be more general > > > proxmox-sendmail/src/lib.rs | 338 +++++++++++++++++++++++++++++++----- > 1 file changed, 299 insertions(+), 39 deletions(-) > > diff --git a/proxmox-sendmail/src/lib.rs b/proxmox-sendmail/src/lib.rs > index f5b04afd..82a8a98e 100644 > --- a/proxmox-sendmail/src/lib.rs > +++ b/proxmox-sendmail/src/lib.rs > @@ -44,23 +44,111 @@ const RFC5987SET: &AsciiSet =3D &CONTROLS > /// base64 encode and hard-wrap the base64 encoded string every 72 chara= cters. this improves > /// compatibility. > fn encode_base64_formatted>(raw: T) -> String { > - const TEXT_WIDTH: usize =3D 72; > - > let encoded =3D proxmox_base64::encode(raw); > - let bytes =3D encoded.as_bytes(); > > - let lines =3D bytes.len().div_ceil(TEXT_WIDTH); > - let mut out =3D Vec::with_capacity(bytes.len() + lines - 1); // acco= unt for 1 newline per line > + format_encoded_text(&encoded, |_| "".into(), "", 0, true) > +} > > - for (line, chunk) in bytes.chunks(TEXT_WIDTH).enumerate() { > +fn encode_filename_formatted(filename: &str) -> String { > + let encoded =3D format!("UTF-8''{}", utf8_percent_encode(filename, R= FC5987SET)); > + > + let format_prefix =3D |l| { > + if let Some(l) =3D l { > + format!("\tfilename*{l}*=3D") > + } else { > + "\tfilename*NN*=3D".into() > + } > + }; > + > + format_encoded_text(&encoded, format_prefix, ";", 0, true) > +} > + > +/// Helper function used to format text to not exceed line limits impose= d by standards such as RFC > +/// 2045 Section 6.8 that specifies that Base64 content transfer encoded= text "must be represented > +/// in lines of no more than 76 characters each". It also allows taking = into account any necessary > +/// pre- and suffixes as well as shortening the first line by a certain = amounts of characters. > +/// > +/// Currently this errs on the side of caution and wraps lines at 72 cha= racters. By doing so, > +/// callers can add a couple of characters to the last line of the text,= which is sometimes needed > +/// to conform to how certain header values in MIME messages need to be = represented. > +/// > +/// Parameters: > +/// * `text`: The text that will be formatted. > +/// * `prefix`: A closure that computes the prefix needed for each line.= It takes an > +/// `Option` as input. If the input is `None`, a prefix that is= used as size hint for > +/// buffer allocation should be returned. `Some(n)` as input, should p= roduce the prefix for the > +/// n-th line of the output text. > +/// * `suffix`: A string slice that will be appended to the end of each = line of output text. > +/// * `shorten_first`: Defines by how many characters the first line wil= l be shortened compared to > +/// the rest of the text. Useful for formatting header values, since = there the first line often > +/// needs to be prefaced with the name of a key in a key value struct= ure or similar. > +/// * `skip_last_suffix`: If true, no suffix will be appended to the las= t line. > +fn format_encoded_text( > + text: &str, > + prefix: F, > + suffix: &str, > + shorten_first: usize, > + skip_last_suffix: bool, > +) -> String > +where > + F: Fn(Option) -> String, > +{ > + const DEFAULT_TEXT_WIDTH: usize =3D 72; > + > + let bytes =3D text.as_bytes(); > + let suffix =3D suffix.as_bytes(); > + > + // compute the number of lines by taking into account skipped charac= ters in the first line and > + // formatting text > + let format_text_len =3D suffix.len() + prefix(None).len(); > + let lines =3D (bytes.len() + shorten_first).div_ceil(DEFAULT_TEXT_WI= DTH - format_text_len); > + > + // bytes + formatting and "\n" per line - no "\n" in the last line > + let mut capacity =3D bytes.len() + (format_text_len + 1) * lines - 1= ; > + > + if skip_last_suffix { > + capacity -=3D suffix.len(); > + } > + > + let mut out =3D Vec::with_capacity(capacity); > + let first_line_length =3D DEFAULT_TEXT_WIDTH - shorten_first - forma= t_text_len; > + > + // format first line if we skip characters > + let (existing_lines, bytes) =3D > + if let Some((first, rest)) =3D bytes.split_at_checked(first_line= _length) { > + out.extend_from_slice(prefix(Some(0)).as_bytes()); > + out.extend_from_slice(first); > + > + if !rest.is_empty() { > + out.extend_from_slice(suffix); > + out.push(b'\n'); > + } else if !skip_last_suffix { > + out.extend_from_slice(suffix); > + } > + > + (1, rest) > + } else { > + (0, bytes) > + }; > + > + // format the rest of the text > + for (line, chunk) in bytes > + .chunks(DEFAULT_TEXT_WIDTH - format_text_len) > + .enumerate() > + { > + let total_lines =3D existing_lines + line; > + out.extend_from_slice(prefix(Some(total_lines)).as_bytes()); > out.extend_from_slice(chunk); > - if line + 1 !=3D lines { > - // do not end last line with newline to give caller control = over doing so. > + > + if total_lines + 1 !=3D lines { > + out.extend_from_slice(suffix); > out.push(b'\n'); > + } else if !skip_last_suffix { > + out.extend_from_slice(suffix); > } > } > > - // SAFETY: base64 encoded, which is 7-bit chars (ASCII) and thus alw= ays valid UTF8 > + // SAFETY: all input slices were valid utf-8 string slices > unsafe { String::from_utf8_unchecked(out) } > } > > @@ -104,24 +192,41 @@ impl Attachment<'_> { > > let mut attachment =3D String::new(); > > - let encoded_filename =3D if self.filename.is_ascii() { > - &self.filename > - } else { > - &format!("=3D?utf-8?B?{}?=3D", proxmox_base64::encode(&self.= filename)) > - }; > - > let _ =3D writeln!(attachment, "\n--{file_boundary}"); > - let _ =3D writeln!( > - attachment, > - "Content-Type: {}; name=3D\"{encoded_filename}\"", > - self.mime, > - ); > > - // both `filename` and `filename*` are included for additional c= ompatibility > + let _ =3D write!(attachment, "Content-Type: {};\n\tname=3D\"", s= elf.mime); > + > + if self.filename.is_ascii() { > + let _ =3D write!(attachment, "{}", &self.filename); > + } else { > + let encoded =3D proxmox_base64::encode(&self.filename); > + let prefix_fn =3D |line| { > + if Some(0) =3D=3D line { > + "=3D?utf-8?B?" > + } else { > + "\t=3D?utf-8?B?" > + } > + .into() > + }; > + > + // minus one here as the first line isn't indented > + let skip_first =3D "\tname=3D\"".len() - 1; > + > + let _ =3D write!( > + attachment, > + "{}", > + format_encoded_text(&encoded, prefix_fn, "?=3D", skip_fi= rst, false) > + ); > + } > + > + // should be fine to add one here, the last line should be at mo= st 72 chars. according to > + // rfc 2045 encoded output lines should be "no more than 76 char= acters each" > + let _ =3D writeln!(attachment, "\""); > + > let _ =3D writeln!( > attachment, > - "Content-Disposition: attachment; filename=3D\"{encoded_file= name}\";\n\tfilename*=3DUTF-8''{}", > - utf8_percent_encode(&self.filename, RFC5987SET) > + "Content-Disposition: attachment;\n{}", > + encode_filename_formatted(&self.filename) > ); > > attachment.push_str("Content-Transfer-Encoding: base64\n\n"); > @@ -541,6 +646,9 @@ impl<'a> Mail<'a> { > mod test { > use super::*; > > + // helper string for formatting tests > + const ALPHA: &str =3D "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs= tuvwxyz1234567890"; > + > /// Compare two multi-line strings, ignoring any line that starts wi= th 'Date:'. > /// > /// The `Date` header is formatted in the local timezone, which mean= s our > @@ -809,9 +917,10 @@ Content-Transfer-Encoding: 7bit > Lorem Ipsum Dolor Sit > Amet > ------_=3D_NextPart_001_1732806251 > -Content-Type: application/octet-stream; name=3D"deadbeef.bin" > -Content-Disposition: attachment; filename=3D"deadbeef.bin"; > - filename*=3DUTF-8''deadbeef.bin > +Content-Type: application/octet-stream; > + name=3D"deadbeef.bin" > +Content-Disposition: attachment; > + filename*0*=3DUTF-8''deadbeef.bin > Content-Transfer-Encoding: base64 > > 3q2+796tvu/erb7v3q3erb7v3q2+796tvu/erd6tvu/erb7v3q2+796t3q2+796tvu/erb7v > @@ -838,7 +947,7 @@ Content-Transfer-Encoding: base64 > ) > .with_recipient_and_name("Receiver Name", "receiver@example.com"= ) > .with_attachment("deadbeef.bin", "application/octet-stream", &bi= n) > - .with_attachment("=F0=9F=90=84=F0=9F=92=80.bin", "image/bmp", &b= in) > + .with_attachment("=F0=9F=90=84=F0=9F=92=80=F0=9F=90=84=F0=9F=92= =80=F0=9F=90=84=F0=9F=92=80=F0=9F=90=84=F0=9F=92=80=F0=9F=90=84=F0=9F=92=80= =F0=9F=90=84=F0=9F=92=80=F0=9F=90=84=F0=9F=92=80=F0=9F=90=84=F0=9F=92=80=F0= =9F=90=84=F0=9F=92=80=F0=9F=90=84=F0=9F=92=80=F0=9F=90=84=F0=9F=92=80=F0=9F= =90=84=F0=9F=92=80=F0=9F=90=84=F0=9F=92=80=F0=9F=90=84=F0=9F=92=80=F0=9F=90= =84=F0=9F=92=80=F0=9F=90=84=F0=9F=92=80.bin", "image/bmp", &bin) > .with_html_alt("\n\t\n\t\tLorem Ipsum Dolor Sit Amet\n\t\n"); > > let body =3D mail.format_mail(1732806251).expect("could not form= at mail"); > @@ -879,17 +988,28 @@ Content-Transfer-Encoding: 7bit > > ------_=3D_NextPart_002_1732806251-- > ------_=3D_NextPart_001_1732806251 > -Content-Type: application/octet-stream; name=3D"deadbeef.bin" > -Content-Disposition: attachment; filename=3D"deadbeef.bin"; > - filename*=3DUTF-8''deadbeef.bin > +Content-Type: application/octet-stream; > + name=3D"deadbeef.bin" > +Content-Disposition: attachment; > + filename*0*=3DUTF-8''deadbeef.bin > Content-Transfer-Encoding: base64 > > 3q2+796tvu/erb7v3q3erb7v3q2+796tvu/erd6tvu/erb7v3q2+796t3q2+796tvu/erb7v > 3q2+796tvu8=3D > ------_=3D_NextPart_001_1732806251 > -Content-Type: image/bmp; name=3D"=3D?utf-8?B?8J+QhPCfkoAuYmlu?=3D" > -Content-Disposition: attachment; filename=3D"=3D?utf-8?B?8J+QhPCfkoAuYml= u?=3D"; > - filename*=3DUTF-8''%F0%9F%90%84%F0%9F%92%80.bin > +Content-Type: image/bmp; > + name=3D"=3D?utf-8?B?8J+QhPCfkoDwn5CE8J+SgPCfkITwn5KA8J+QhPCfkoDwn5CE8J+= Sg?=3D > + =3D?utf-8?B?PCfkITwn5KA8J+QhPCfkoDwn5CE8J+SgPCfkITwn5KA8J+QhPCfkoDwn5CE= ?=3D > + =3D?utf-8?B?8J+SgPCfkITwn5KA8J+QhPCfkoDwn5CE8J+SgPCfkITwn5KA8J+QhPCfkoA= ?=3D > + =3D?utf-8?B?uYmlu?=3D" > +Content-Disposition: attachment; > + filename*0*=3DUTF-8''%F0%9F%90%84%F0%9F%92%80%F0%9F%90%84%F0%9F%92%80%F= ; > + filename*1*=3D0%9F%90%84%F0%9F%92%80%F0%9F%90%84%F0%9F%92%80%F0%9F%90%8= ; > + filename*2*=3D4%F0%9F%92%80%F0%9F%90%84%F0%9F%92%80%F0%9F%90%84%F0%9F%9= ; > + filename*3*=3D2%80%F0%9F%90%84%F0%9F%92%80%F0%9F%90%84%F0%9F%92%80%F0%9= ; > + filename*4*=3DF%90%84%F0%9F%92%80%F0%9F%90%84%F0%9F%92%80%F0%9F%90%84%F= ; > + filename*5*=3D0%9F%92%80%F0%9F%90%84%F0%9F%92%80%F0%9F%90%84%F0%9F%92%8= ; > + filename*6*=3D0%F0%9F%90%84%F0%9F%92%80%F0%9F%90%84%F0%9F%92%80.bin > Content-Transfer-Encoding: base64 > > 3q2+796tvu/erb7v3q3erb7v3q2+796tvu/erd6tvu/erb7v3q2+796t3q2+796tvu/erb7v > @@ -997,17 +1117,19 @@ PGh0bWwgbGFuZz0iZGUtYXQiPjxoZWFkPjwvaGVhZD48Ym9keT= 4KCTxwcmU+CgkJTG9yZW0g > SXBzdW0gRMO2bG9yIFNpdCBBbWV0Cgk8L3ByZT4KPC9ib2R5PjwvaHRtbD4=3D > ------_=3D_NextPart_002_1732806251-- > ------_=3D_NextPart_001_1732806251 > -Content-Type: application/octet-stream; name=3D"deadbeef.bin" > -Content-Disposition: attachment; filename=3D"deadbeef.bin"; > - filename*=3DUTF-8''deadbeef.bin > +Content-Type: application/octet-stream; > + name=3D"deadbeef.bin" > +Content-Disposition: attachment; > + filename*0*=3DUTF-8''deadbeef.bin > Content-Transfer-Encoding: base64 > > 3q2+796tvu/erb7v3q3erb7v3q2+796tvu/erd6tvu/erb7v3q2+796t3q2+796tvu/erb7v > 3q2+796tvu8=3D > ------_=3D_NextPart_001_1732806251 > -Content-Type: image/bmp; name=3D"=3D?utf-8?B?8J+QhPCfkoAuYmlu?=3D" > -Content-Disposition: attachment; filename=3D"=3D?utf-8?B?8J+QhPCfkoAuYml= u?=3D"; > - filename*=3DUTF-8''%F0%9F%90%84%F0%9F%92%80.bin > +Content-Type: image/bmp; > + name=3D"=3D?utf-8?B?8J+QhPCfkoAuYmlu?=3D" > +Content-Disposition: attachment; > + filename*0*=3DUTF-8''%F0%9F%90%84%F0%9F%92%80.bin > Content-Transfer-Encoding: base64 > > 3q2+796tvu/erb7v3q3erb7v3q2+796tvu/erd6tvu/erb7v3q2+796t3q2+796tvu/erb7v > @@ -1015,4 +1137,142 @@ Content-Transfer-Encoding: base64 > ------_=3D_NextPart_001_1732806251--"#, > ) > } > + > + #[test] > + fn format_encoded_text_correctly() { > + let simple =3D format_encoded_text(ALPHA, |_| "".to_string(), ""= , 0, true); > + > + assert_eq!(simple, ALPHA); > + } > + > + #[test] > + fn format_encoded_text_multiple_lines() { > + let multiple_lines =3D format_encoded_text( > + &format!("{ALPHA}{ALPHA}{ALPHA}"), > + |_| "".to_string(), > + "", > + 0, > + true, > + ); > + > + let out =3D r#" > +ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJ > +KLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRST > +UVWXYZabcdefghijklmnopqrstuvwxyz1234567890"# > + .trim(); > + > + assert_eq!(multiple_lines, out); > + } > + > + #[test] > + fn format_encoded_text_suffixed() { > + let suffixed =3D format_encoded_text(ALPHA, |_| "".to_string(), = ";", 0, false); > + > + let out =3D "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxy= z1234567890;"; > + > + assert_eq!(suffixed, out); > + > + let suffixed =3D format_encoded_text(ALPHA, |_| "".to_string(), = ";", 0, true); > + > + let out =3D "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxy= z1234567890"; > + > + assert_eq!(suffixed, out); > + > + let suffixed =3D format_encoded_text( > + &format!("{ALPHA}{ALPHA}{ALPHA}"), > + |_| "".to_string(), > + ";", > + 0, > + false, > + ); > + > + let out =3D r#" > +ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHI; > +JKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQR; > +STUVWXYZabcdefghijklmnopqrstuvwxyz1234567890;"# > + .trim(); > + > + assert_eq!(suffixed, out); > + > + let suffixed =3D format_encoded_text( > + &format!("{ALPHA}{ALPHA}{ALPHA}"), > + |_| "".to_string(), > + ";", > + 0, > + true, > + ); > + > + let out =3D r#" > +ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHI; > +JKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQR; > +STUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"# > + .trim(); > + > + assert_eq!(suffixed, out); > + } > + > + #[test] > + fn format_encoded_text_prefixed() { > + let suffixed =3D format_encoded_text(ALPHA, |_| "=3D=3D".to_stri= ng(), "", 0, false); > + > + let out =3D "=3D=3DABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs= tuvwxyz1234567890"; > + > + assert_eq!(suffixed, out); > + > + let suffixed =3D format_encoded_text( > + &format!("{ALPHA}{ALPHA}{ALPHA}"), > + |_| "=3D=3D".to_string(), > + "", > + 0, > + false, > + ); > + > + let out =3D r#" > +=3D=3DABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCD= EFGH > +=3D=3DIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKL= MNOP > +=3D=3DQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"# > + .trim(); > + > + assert_eq!(suffixed, out); > + > + let suffixed =3D format_encoded_text( > + &format!("{ALPHA}{ALPHA}{ALPHA}"), > + |i| { > + if let Some(n) =3D i { > + format!("=3D{n}=3D") > + } else { > + "=3D=3D=3D".to_string() > + } > + }, > + "", > + 0, > + false, > + ); > + > + let out =3D r#" > +=3D0=3DABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABC= DEFG > +=3D1=3DHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJ= KLMN > +=3D2=3DOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"# > + .trim(); > + > + assert_eq!(suffixed, out); > + } > + > + #[test] > + fn format_encoded_text_skip_first() { > + let suffixed =3D format_encoded_text(ALPHA, |_| "".to_string(), = "", 5, false); > + > + let out =3D "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxy= z1234567890"; > + > + assert_eq!(suffixed, out); > + > + let suffixed =3D format_encoded_text(ALPHA, |_| "".to_string(), = "", 20, false); > + > + let out =3D r#" > +ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz > +1234567890"# > + .trim(); > + > + assert_eq!(suffixed, out); > + } > } > -- > 2.47.3