Files
inbuxa-migrate/tests/integration/data.rs
T
2026-05-29 18:02:15 +02:00

476 lines
14 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use flate2::read::GzDecoder;
use mail_parser::mailbox::mbox::MessageIterator;
use super::error::{ContainerError, ContainerResult};
fn resources_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("resources")
}
#[derive(Debug, Clone)]
pub struct MboxMessage {
pub raw: Vec<u8>,
pub received_at: i64,
}
pub fn load_mbox(limit: usize) -> ContainerResult<Vec<MboxMessage>> {
let path = resources_dir().join("mailbox.gz");
if path.exists() {
let file = std::fs::File::open(&path)?;
let mut decoder = GzDecoder::new(file);
let mut bytes = Vec::new();
decoder.read_to_end(&mut bytes)?;
let reader = BufReader::new(std::io::Cursor::new(bytes));
let mut out = Vec::new();
for item in MessageIterator::new(reader) {
let message = item.map_err(|e| ContainerError::Resource(format!("mbox parse: {e}")))?;
let received_at = message.internal_date() as i64;
out.push(MboxMessage {
raw: message.unwrap_contents(),
received_at,
});
if out.len() >= limit {
break;
}
}
if !out.is_empty() {
return Ok(out);
}
}
if let Ok(takeout) = load_takeout_mbox("all_mail_including_spam_and_trash.mbox")
&& !takeout.is_empty()
{
return Ok(takeout.into_iter().take(limit).collect());
}
Ok(synth_messages(limit))
}
#[derive(Debug, Clone)]
pub struct RawFixture {
pub name: String,
pub bytes: Vec<u8>,
}
pub fn load_vcards() -> ContainerResult<Vec<RawFixture>> {
Ok(synth_vcards(48))
}
pub fn load_icals() -> ContainerResult<Vec<RawFixture>> {
Ok(synth_icals(48))
}
pub fn load_takeout_mbox(name: &str) -> ContainerResult<Vec<MboxMessage>> {
let path = resources_dir().join("takeout").join(name);
let bytes = std::fs::read(&path)
.map_err(|e| ContainerError::Resource(format!("open {}: {e}", path.display())))?;
let reader = BufReader::new(std::io::Cursor::new(bytes));
let mut out = Vec::new();
for item in MessageIterator::new(reader) {
let message = item.map_err(|e| ContainerError::Resource(format!("mbox parse: {e}")))?;
let received_at = message.internal_date() as i64;
out.push(MboxMessage {
raw: message.unwrap_contents(),
received_at,
});
}
Ok(out)
}
fn synth_messages(n: usize) -> Vec<MboxMessage> {
let mut out = Vec::with_capacity(n);
for i in 0..n {
let body = format!(
"From: sender{i}@vandelay.test\r\n\
To: [email protected]\r\n\
Subject: Synthetic test message {i}\r\n\
Message-ID: <synth-{i}@vandelay.test>\r\n\
Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\
MIME-Version: 1.0\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
\r\n\
This is synthetic seed message #{i} for vandelay container tests.\r\n",
);
out.push(MboxMessage {
raw: body.into_bytes(),
received_at: 0,
});
}
out
}
const VCARD_VARIANTS: &[fn(usize) -> String] = &[
vcard_v3_basic,
vcard_v3_full,
vcard_v3_multi_email,
vcard_v3_org_and_title,
vcard_v4_basic,
vcard_v4_with_address,
vcard_v4_nickname,
vcard_v4_birthday,
];
fn synth_vcards(n: usize) -> Vec<RawFixture> {
let mut out = Vec::with_capacity(n);
for i in 0..n {
let body = VCARD_VARIANTS[i % VCARD_VARIANTS.len()](i);
out.push(RawFixture {
name: format!("synth-{i:03}"),
bytes: body.into_bytes(),
});
}
out
}
fn vcard_v3_basic(i: usize) -> String {
format!(
"BEGIN:VCARD\r\n\
VERSION:3.0\r\n\
UID:vandelay-card-basic-{i}\r\n\
FN:Basic Contact {i}\r\n\
N:Contact{i};Basic;;;\r\n\
EMAIL;TYPE=INTERNET:basic-{i}@vandelay.test\r\n\
END:VCARD\r\n"
)
}
fn vcard_v3_full(i: usize) -> String {
format!(
"BEGIN:VCARD\r\n\
VERSION:3.0\r\n\
UID:vandelay-card-full-{i}\r\n\
FN:Full Contact {i}\r\n\
N:Contact{i};Full;Middle;;\r\n\
EMAIL;TYPE=INTERNET;TYPE=WORK:full-{i}@vandelay.test\r\n\
TEL;TYPE=CELL:+15550010{i:03}\r\n\
ORG:Vandelay Test\r\n\
TITLE:Senior Test Engineer\r\n\
URL:https://vandelay.test/{i}\r\n\
NOTE:Synthetic full v3 contact #{i}\r\n\
END:VCARD\r\n"
)
}
fn vcard_v3_multi_email(i: usize) -> String {
format!(
"BEGIN:VCARD\r\n\
VERSION:3.0\r\n\
UID:vandelay-card-multi-{i}\r\n\
FN:Multi Email {i}\r\n\
N:Contact{i};MultiEmail;;;\r\n\
EMAIL;TYPE=INTERNET;TYPE=HOME:multi-{i}[email protected]\r\n\
EMAIL;TYPE=INTERNET;TYPE=WORK:multi-{i}[email protected]\r\n\
END:VCARD\r\n"
)
}
fn vcard_v3_org_and_title(i: usize) -> String {
format!(
"BEGIN:VCARD\r\n\
VERSION:3.0\r\n\
UID:vandelay-card-org-{i}\r\n\
FN:Org Contact {i}\r\n\
N:Contact{i};Org;;Dr.;PhD\r\n\
ORG:Vandelay Industries;Engineering;Test Group\r\n\
TITLE:Principal Researcher\r\n\
EMAIL;TYPE=INTERNET:org-{i}@vandelay.test\r\n\
END:VCARD\r\n"
)
}
fn vcard_v4_basic(i: usize) -> String {
format!(
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
UID:urn:uuid:00000000-0000-4000-a000-0000{i:08x}\r\n\
FN:V4 Basic {i}\r\n\
N:Contact{i};V4Basic;;;\r\n\
EMAIL:v4-basic-{i}@vandelay.test\r\n\
END:VCARD\r\n"
)
}
fn vcard_v4_with_address(i: usize) -> String {
format!(
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
UID:urn:uuid:00000000-0000-4000-b000-0000{i:08x}\r\n\
FN:V4 Address {i}\r\n\
N:Contact{i};V4Address;;;\r\n\
EMAIL:v4-address-{i}@vandelay.test\r\n\
ADR;TYPE=home:;;{i} Test Street;Springfield;OR;97477;US\r\n\
END:VCARD\r\n"
)
}
fn vcard_v4_nickname(i: usize) -> String {
format!(
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
UID:urn:uuid:00000000-0000-4000-c000-0000{i:08x}\r\n\
FN:V4 Nick {i}\r\n\
N:Contact{i};V4Nick;;;\r\n\
NICKNAME:Nicky{i}\r\n\
EMAIL:v4-nick-{i}@vandelay.test\r\n\
END:VCARD\r\n"
)
}
fn vcard_v4_birthday(i: usize) -> String {
let year = 1970 + (i % 30);
let month = (i % 12) + 1;
let day = (i % 28) + 1;
format!(
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
UID:urn:uuid:00000000-0000-4000-d000-0000{i:08x}\r\n\
FN:V4 Birthday {i}\r\n\
N:Contact{i};V4Birthday;;;\r\n\
BDAY:{year:04}{month:02}{day:02}\r\n\
EMAIL:v4-bday-{i}@vandelay.test\r\n\
END:VCARD\r\n"
)
}
const ICAL_VARIANTS: &[fn(usize) -> String] = &[
ical_simple_dt,
ical_all_day,
ical_multi_day_dt,
ical_with_organizer_attendees,
ical_with_categories_location,
ical_recurring_daily,
ical_recurring_weekly_until,
ical_with_alarm,
];
fn synth_icals(n: usize) -> Vec<RawFixture> {
let mut out = Vec::with_capacity(n);
for i in 0..n {
let body = ICAL_VARIANTS[i % ICAL_VARIANTS.len()](i);
out.push(RawFixture {
name: format!("synth-{i:03}"),
bytes: body.into_bytes(),
});
}
out
}
fn ical_simple_dt(i: usize) -> String {
let month = ((i % 12) + 1) as u32;
let day = ((i % 28) + 1) as u32;
let hour = (i % 24) as u32;
format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//vandelay//container-test//EN\r\n\
BEGIN:VEVENT\r\n\
UID:vandelay-simple-{i}@vandelay.test\r\n\
DTSTAMP:20250101T120000Z\r\n\
DTSTART:2025{month:02}{day:02}T{hour:02}0000Z\r\n\
DTEND:2025{month:02}{day:02}T{hour:02}3000Z\r\n\
SUMMARY:Simple datetime event {i}\r\n\
DESCRIPTION:Simple synthetic event #{i}\r\n\
END:VEVENT\r\n\
END:VCALENDAR\r\n"
)
}
fn ical_all_day(i: usize) -> String {
let month = ((i % 12) + 1) as u32;
let day = ((i % 28) + 1) as u32;
let next_day = day + 1;
format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//vandelay//container-test//EN\r\n\
BEGIN:VEVENT\r\n\
UID:vandelay-allday-{i}@vandelay.test\r\n\
DTSTAMP:20250101T120000Z\r\n\
DTSTART;VALUE=DATE:2025{month:02}{day:02}\r\n\
DTEND;VALUE=DATE:2025{month:02}{next_day:02}\r\n\
SUMMARY:All-day event {i}\r\n\
END:VEVENT\r\n\
END:VCALENDAR\r\n"
)
}
fn ical_multi_day_dt(i: usize) -> String {
let month = ((i % 12) + 1) as u32;
let day = ((i % 27) + 1) as u32;
format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//vandelay//container-test//EN\r\n\
BEGIN:VEVENT\r\n\
UID:vandelay-multiday-{i}@vandelay.test\r\n\
DTSTAMP:20250101T120000Z\r\n\
DTSTART:2025{month:02}{day:02}T090000Z\r\n\
DTEND:2025{month:02}{day:02}T170000Z\r\n\
SUMMARY:Workshop session {i}\r\n\
DESCRIPTION:Multi-hour workshop with description.\r\n\
END:VEVENT\r\n\
END:VCALENDAR\r\n"
)
}
fn ical_with_organizer_attendees(i: usize) -> String {
let month = ((i % 12) + 1) as u32;
let day = ((i % 28) + 1) as u32;
format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//vandelay//container-test//EN\r\n\
BEGIN:VEVENT\r\n\
UID:vandelay-meeting-{i}@vandelay.test\r\n\
DTSTAMP:20250101T120000Z\r\n\
DTSTART:2025{month:02}{day:02}T140000Z\r\n\
DTEND:2025{month:02}{day:02}T150000Z\r\n\
SUMMARY:Sync meeting {i}\r\n\
ORGANIZER;CN=Organiser:mailto:organiser-{i}@vandelay.test\r\n\
ATTENDEE;CN=Alice;PARTSTAT=ACCEPTED:mailto:alice-{i}@vandelay.test\r\n\
ATTENDEE;CN=Bob;PARTSTAT=NEEDS-ACTION:mailto:bob-{i}@vandelay.test\r\n\
END:VEVENT\r\n\
END:VCALENDAR\r\n"
)
}
fn ical_with_categories_location(i: usize) -> String {
let month = ((i % 12) + 1) as u32;
let day = ((i % 28) + 1) as u32;
format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//vandelay//container-test//EN\r\n\
BEGIN:VEVENT\r\n\
UID:vandelay-cat-{i}@vandelay.test\r\n\
DTSTAMP:20250101T120000Z\r\n\
DTSTART:2025{month:02}{day:02}T100000Z\r\n\
DTEND:2025{month:02}{day:02}T110000Z\r\n\
SUMMARY:Categorised event {i}\r\n\
LOCATION:Test Lab\\, Room {i}\r\n\
CATEGORIES:VANDELAY,TEST,FIXTURE\r\n\
END:VEVENT\r\n\
END:VCALENDAR\r\n"
)
}
fn ical_recurring_daily(i: usize) -> String {
let month = ((i % 12) + 1) as u32;
let day = ((i % 28) + 1) as u32;
format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//vandelay//container-test//EN\r\n\
BEGIN:VEVENT\r\n\
UID:vandelay-daily-{i}@vandelay.test\r\n\
DTSTAMP:20250101T120000Z\r\n\
DTSTART:2025{month:02}{day:02}T070000Z\r\n\
DTEND:2025{month:02}{day:02}T073000Z\r\n\
RRULE:FREQ=DAILY;COUNT=10\r\n\
SUMMARY:Daily standup {i}\r\n\
END:VEVENT\r\n\
END:VCALENDAR\r\n"
)
}
fn ical_recurring_weekly_until(i: usize) -> String {
let month = ((i % 12) + 1) as u32;
let day = ((i % 28) + 1) as u32;
let until_month = (month % 12) + 1;
format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//vandelay//container-test//EN\r\n\
BEGIN:VEVENT\r\n\
UID:vandelay-weekly-{i}@vandelay.test\r\n\
DTSTAMP:20250101T120000Z\r\n\
DTSTART:2025{month:02}{day:02}T160000Z\r\n\
DTEND:2025{month:02}{day:02}T170000Z\r\n\
RRULE:FREQ=WEEKLY;UNTIL=2025{until_month:02}{day:02}T160000Z;BYDAY=MO\r\n\
SUMMARY:Weekly retro {i}\r\n\
END:VEVENT\r\n\
END:VCALENDAR\r\n"
)
}
fn ical_with_alarm(i: usize) -> String {
let month = ((i % 12) + 1) as u32;
let day = ((i % 28) + 1) as u32;
format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//vandelay//container-test//EN\r\n\
BEGIN:VEVENT\r\n\
UID:vandelay-alarm-{i}@vandelay.test\r\n\
DTSTAMP:20250101T120000Z\r\n\
DTSTART:2025{month:02}{day:02}T080000Z\r\n\
DTEND:2025{month:02}{day:02}T083000Z\r\n\
SUMMARY:Event with alarm {i}\r\n\
BEGIN:VALARM\r\n\
ACTION:DISPLAY\r\n\
DESCRIPTION:Reminder {i}\r\n\
TRIGGER:-PT15M\r\n\
END:VALARM\r\n\
END:VEVENT\r\n\
END:VCALENDAR\r\n"
)
}
pub fn malformed_ical(name: &str) -> RawFixture {
let body = format!(
"BEGIN:VCALENDAR\r\n\
VERSION:2.0\r\n\
PRODID:-//vandelay//broken//EN\r\n\
BEGIN:VEVENT\r\n\
UID:broken-{name}@vandelay.test\r\n\
DTSTAMP:20250101T120000Z\r\n\
DTSTART:NOT-A-DATE\r\n\
RRULE:FREQ=BOGUS;INTERVAL=oops\r\n\
END:NOT-A-VEVENT\r\n",
);
RawFixture {
name: format!("broken-{name}"),
bytes: body.into_bytes(),
}
}
pub fn rewrite_uid(bytes: &[u8], suffix: &str) -> Option<(Vec<u8>, String)> {
let text = std::str::from_utf8(bytes).ok()?;
let mut out = String::with_capacity(text.len() + 32);
let mut new_uid: Option<String> = None;
for line in text.lines() {
if let Some(rest) = line.strip_prefix("UID:") {
let combined = format!("{rest}-{suffix}");
if new_uid.is_none() {
new_uid = Some(combined.clone());
}
out.push_str("UID:");
out.push_str(&combined);
} else {
out.push_str(line);
}
out.push_str("\r\n");
}
let uid = new_uid?;
Some((out.into_bytes(), uid))
}
pub fn extract_uid(bytes: &[u8]) -> Option<String> {
let text = std::str::from_utf8(bytes).ok()?;
for line in text.lines() {
let l = line.trim_end_matches('\r');
if let Some(rest) = l.strip_prefix("UID:") {
return Some(rest.to_owned());
}
}
None
}