diff --git a/Cargo.lock b/Cargo.lock index 2a694df9..1de2b100 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1716,6 +1716,7 @@ name = "imap" version = "0.1.0" dependencies = [ "ahash 0.8.3", + "dashmap", "directory", "imap_proto", "jmap", @@ -1860,6 +1861,7 @@ dependencies = [ "base64 0.21.2", "bincode", "chrono", + "dashmap", "directory", "ece", "form-data", @@ -2162,9 +2164,11 @@ name = "mail-server" version = "0.3.0" dependencies = [ "directory", + "imap", "jemallocator", "jmap", "jmap_proto", + "managesieve", "smtp", "store", "tokio", @@ -2172,6 +2176,31 @@ dependencies = [ "utils", ] +[[package]] +name = "managesieve" +version = "0.1.0" +dependencies = [ + "ahash 0.8.3", + "bincode", + "directory", + "imap", + "imap_proto", + "jmap", + "jmap_proto", + "mail-parser", + "mail-send", + "md5", + "parking_lot", + "rustls 0.21.2", + "rustls-pemfile", + "sieve-rs", + "store", + "tokio", + "tokio-rustls 0.24.1", + "tracing", + "utils", +] + [[package]] name = "maplit" version = "1.0.2" @@ -4160,6 +4189,7 @@ dependencies = [ "mail-auth", "mail-parser", "mail-send", + "managesieve", "num_cpus", "rayon", "reqwest", @@ -4717,6 +4747,8 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" name = "utils" version = "0.1.0" dependencies = [ + "ahash 0.8.3", + "dashmap", "mail-auth", "mail-send", "opentelemetry", diff --git a/Cargo.toml b/Cargo.toml index c0cc89f2..9158032c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ store = { path = "crates/store" } jmap = { path = "crates/jmap" } jmap_proto = { path = "crates/jmap-proto" } smtp = { path = "crates/smtp", features = ["local_delivery"] } +imap = { path = "crates/imap" } +managesieve = { path = "crates/managesieve" } directory = { path = "crates/directory" } utils = { path = "crates/utils" } tokio = { version = "1.23", features = ["full"] } @@ -40,6 +42,7 @@ members = [ "crates/imap", "crates/imap-proto", "crates/smtp", + "crates/managesieve", "crates/store", "crates/directory", "crates/utils", diff --git a/crates/imap-proto/src/protocol/acl.rs b/crates/imap-proto/src/protocol/acl.rs index cec20db8..41696fef 100644 --- a/crates/imap-proto/src/protocol/acl.rs +++ b/crates/imap-proto/src/protocol/acl.rs @@ -52,6 +52,8 @@ use std::fmt::Display; +use jmap_proto::types::acl::Acl; + use crate::utf7::utf7_encode; use super::quoted_string; @@ -243,47 +245,23 @@ impl Display for Rights { } } -/* -pub trait AsImapRights { - fn as_imap_rights(&self) -> Vec; -} - -impl AsImapRights for MailboxRights { - fn as_imap_rights(&self) -> Vec { - let mut rights = Vec::with_capacity(5); - if self.may_read_items() { - rights.push(Rights::Read); - rights.push(Rights::Lookup); +impl From for Acl { + fn from(value: Rights) -> Self { + match value { + Rights::Lookup => Acl::Read, + Rights::Read => Acl::ReadItems, + Rights::Seen => Acl::ModifyItems, + Rights::Write => Acl::ModifyItems, + Rights::Insert => Acl::AddItems, + Rights::Post => Acl::Submit, + Rights::CreateMailbox => Acl::CreateChild, + Rights::DeleteMailbox => Acl::Delete, + Rights::DeleteMessages => Acl::RemoveItems, + Rights::Expunge => Acl::RemoveItems, + Rights::Administer => Acl::Administer, } - if self.may_add_items() { - rights.push(Rights::Insert); - } - if self.may_remove_items() { - rights.push(Rights::DeleteMessages); - rights.push(Rights::Expunge); - } - if self.may_set_seen() { - rights.push(Rights::Seen); - } - if self.may_set_keywords() { - rights.push(Rights::Write); - } - if self.may_create_child() { - rights.push(Rights::CreateMailbox); - } - if self.may_rename() { - rights.push(Rights::DeleteMailbox); - } - if self.may_delete() { - rights.push(Rights::DeleteMailbox); - } - if self.may_submit() { - rights.push(Rights::Post); - } - rights } } -*/ #[cfg(test)] mod tests { diff --git a/crates/imap-proto/src/receiver.rs b/crates/imap-proto/src/receiver.rs index 2dc90c22..1ce2eb40 100644 --- a/crates/imap-proto/src/receiver.rs +++ b/crates/imap-proto/src/receiver.rs @@ -1080,133 +1080,4 @@ mod tests { } } } - - #[test] - fn receiver_parse_managesieve() { - let implement = "true"; - /* - use crate::managesieve::Command; - - let mut receiver = Receiver::new().with_start_state(State::Command { is_uid: false }); - - for (frames, expected_requests) in [ - ( - vec!["Authenticate \"DIGEST-MD5\"\r\n"], - vec![Request { - tag: "".to_string(), - command: Command::Authenticate, - tokens: vec![Token::Argument(b"DIGEST-MD5".to_vec())], - }], - ), - ( - vec![ - " AUTHENTICATE \"GSSAPI\" {56+}\r\n", - "cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA==\r\n", - ], - vec![Request { - tag: "".to_string(), - command: Command::Authenticate, - tokens: vec![ - Token::Argument(b"GSSAPI".to_vec()), - Token::Argument( - b"cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA==".to_vec(), - ), - ], - }], - ), - ( - vec!["Authenticate \"PLAIN\" \"QJIrweAPyo6Q1T9xu\"\r\n"], - vec![Request { - tag: "".to_string(), - command: Command::Authenticate, - tokens: vec![ - Token::Argument(b"PLAIN".to_vec()), - Token::Argument(b"QJIrweAPyo6Q1T9xu".to_vec()), - ], - }], - ), - ( - vec!["StartTls\r\n"], - vec![Request { - tag: "".to_string(), - command: Command::StartTls, - tokens: vec![], - }], - ), - ( - vec!["HAVESPACE \"myscript\" 999999\r\n"], - vec![Request { - tag: "".to_string(), - command: Command::HaveSpace, - tokens: vec![ - Token::Argument(b"myscript".to_vec()), - Token::Argument(b"999999".to_vec()), - ], - }], - ), - ( - vec![ - "Putscript \"foo\" {31+}\r\n", - "#comment\r\n", - "InvalidSieveCommand\r\n\r\n", - ], - vec![Request { - tag: "".to_string(), - command: Command::PutScript, - tokens: vec![ - Token::Argument(b"foo".to_vec()), - Token::Argument(b"#comment\r\nInvalidSieveCommand\r\n".to_vec()), - ], - }], - ), - ( - vec!["Listscripts\r\n"], - vec![Request { - tag: "".to_string(), - command: Command::ListScripts, - tokens: vec![], - }], - ), - ( - vec!["Setactive \"baz\"\r\n"], - vec![Request { - tag: "".to_string(), - command: Command::SetActive, - tokens: vec![Token::Argument(b"baz".to_vec())], - }], - ), - ( - vec!["Renamescript \"foo\" \"bar\"\r\n"], - vec![Request { - tag: "".to_string(), - command: Command::RenameScript, - tokens: vec![ - Token::Argument(b"foo".to_vec()), - Token::Argument(b"bar".to_vec()), - ], - }], - ), - ( - vec!["NOOP \"STARTTLS-SYNC-42\"\r\n"], - vec![Request { - tag: "".to_string(), - command: Command::Noop, - tokens: vec![Token::Argument(b"STARTTLS-SYNC-42".to_vec())], - }], - ), - ] { - let mut requests = Vec::new(); - for frame in &frames { - let mut bytes = frame.as_bytes().iter(); - loop { - match receiver.parse(&mut bytes) { - Ok(request) => requests.push(request), - Err(Error::NeedsMoreData | Error::NeedsLiteral { .. }) => break, - Err(err) => panic!("{:?} for frames {:#?}", err, frames), - } - } - } - assert_eq!(requests, expected_requests, "{:#?}", frames); - }*/ - } } diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index e647391a..b5d55157 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -21,6 +21,7 @@ parking_lot = "0.12" tracing = "0.1" ahash = { version = "0.8" } md5 = "0.7.0" +dashmap = "5.4" [features] test_mode = [] diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index 3e1fa307..8bce0f06 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -25,11 +25,14 @@ use std::{iter::Peekable, sync::Arc, vec::IntoIter}; use imap_proto::{ receiver::{self, Request}, - Command, StatusResponse, + Command, ResponseCode, StatusResponse, }; +use jmap::auth::rate_limit::AuthenticatedLimiter; +use parking_lot::Mutex; use tokio::io::AsyncRead; +use utils::listener::limiter::{ConcurrencyLimiter, RateLimiter}; -use super::{SelectedMailbox, Session, SessionData, State}; +use super::{SelectedMailbox, Session, SessionData, State, IMAP}; impl Session { pub async fn ingest(&mut self, bytes: &[u8]) -> crate::Result { @@ -38,6 +41,11 @@ impl Session { println!("<- {:?}", &line[..std::cmp::min(line.len(), 100)]); }*/ + tracing::trace!(parent: &self.span, + event = "read", + data = std::str::from_utf8(bytes).unwrap_or("[invalid UTF8]"), + size = bytes.len()); + let mut bytes = bytes.iter(); let mut requests = Vec::with_capacity(2); let mut needs_literal = None; @@ -70,112 +78,121 @@ impl Session { while let Some(request) = requests.next() { match request.command { Command::List | Command::Lsub => { - //self.handle_list(request).await?; + self.handle_list(request).await?; } Command::Select | Command::Examine => { - //self.handle_select(request).await?; + self.handle_select(request).await?; } Command::Create => { - //self.handle_create(group_requests(&mut requests, vec![request])).await?; + self.handle_create(group_requests(&mut requests, vec![request])) + .await?; } Command::Delete => { - //self.handle_delete(group_requests(&mut requests, vec![request])).await?; + self.handle_delete(group_requests(&mut requests, vec![request])) + .await?; } Command::Rename => { - //self.handle_rename(request).await?; + self.handle_rename(request).await?; } Command::Status => { - //self.handle_status(request).await?; + self.handle_status(request).await?; } Command::Append => { - //self.handle_append(request).await?; + self.handle_append(request).await?; } Command::Close => { - //self.handle_close(request).await?; + self.handle_close(request).await?; } Command::Unselect => { - //self.handle_unselect(request).await?; + self.handle_unselect(request).await?; } Command::Expunge(is_uid) => { - //self.handle_expunge(request, is_uid).await?; + self.handle_expunge(request, is_uid).await?; } Command::Search(is_uid) => { - //self.handle_search(request, false, is_uid).await?; + self.handle_search(request, false, is_uid).await?; } Command::Fetch(is_uid) => { - //self.handle_fetch(request, is_uid).await?; + self.handle_fetch(request, is_uid).await?; } Command::Store(is_uid) => { - //self.handle_store(request, is_uid).await?; + self.handle_store(request, is_uid).await?; } Command::Copy(is_uid) => { - //self.handle_copy_move(request, false, is_uid).await?; + self.handle_copy_move(request, false, is_uid).await?; } Command::Move(is_uid) => { - //self.handle_copy_move(request, true, is_uid).await?; + self.handle_copy_move(request, true, is_uid).await?; } Command::Sort(is_uid) => { - //self.handle_search(request, true, is_uid).await?; + self.handle_search(request, true, is_uid).await?; } Command::Thread(is_uid) => { - //self.handle_thread(request, is_uid).await?; + self.handle_thread(request, is_uid).await?; } Command::Idle => { - //self.handle_idle(request).await?; + self.handle_idle(request).await?; } Command::Subscribe => { - //self.handle_subscribe(request, true).await?; + self.handle_subscribe(request, true).await?; } Command::Unsubscribe => { - //self.handle_subscribe(request, false).await?; + self.handle_subscribe(request, false).await?; } Command::Namespace => { - //self.handle_namespace(request).await?; + self.handle_namespace(request).await?; } Command::Authenticate => { - //self.handle_authenticate(request).await?; + self.handle_authenticate(request).await?; } Command::Login => { - //self.handle_login(request).await?; + self.handle_login(request).await?; } Command::Capability => { - //self.handle_capability(request).await?; + self.handle_capability(request).await?; } Command::Enable => { - //self.handle_enable(request).await?; + self.handle_enable(request).await?; } Command::StartTls => { - //return self.handle_starttls(request).await; + return self + .write_bytes( + StatusResponse::ok("Begin TLS negotiation now") + .with_tag(request.tag) + .into_bytes(), + ) + .await + .map(|_| true); } Command::Noop => { - //self.handle_noop(request, false).await?; + self.handle_noop(request).await?; } Command::Check => { - //self.handle_noop(request, true).await?; + self.handle_noop(request).await?; } Command::Logout => { - //self.handle_logout(request).await?; + self.handle_logout(request).await?; } Command::SetAcl => { - //self.handle_set_acl(request).await?; + self.handle_set_acl(request).await?; } Command::DeleteAcl => { - //self.handle_delete_acl(request).await?; + self.handle_set_acl(request).await?; } Command::GetAcl => { - //self.handle_get_acl(request).await?; + self.handle_get_acl(request).await?; } Command::ListRights => { - //self.handle_list_rights(request).await?; + self.handle_list_rights(request).await?; } Command::MyRights => { - //self.handle_my_rights(request).await?; + self.handle_my_rights(request).await?; } Command::Unauthenticate => { - //self.handle_unauthenticate(request).await?; + self.handle_unauthenticate(request).await?; } Command::Id => { - //self.handle_id(request).await?; + self.handle_id(request).await?; } } } @@ -211,6 +228,21 @@ trait IsAllowed: Sized { impl IsAllowed for Request { fn is_allowed(self, state: &State, is_tls: bool) -> Result { + // Rate limit request + if let State::Authenticated { data } | State::Selected { data, .. } = state { + if !data + .imap + .get_authenticated_limiter(data.account_id) + .lock() + .request_limiter + .is_allowed() + { + return Err(StatusResponse::no("Too many requests") + .with_tag(self.tag) + .with_code(ResponseCode::Limit)); + } + } + match &self.command { Command::Capability | Command::Noop | Command::Logout | Command::Id => Ok(self), Command::StartTls => { @@ -348,3 +380,23 @@ impl State { matches!(self, State::Selected { .. }) } } + +impl IMAP { + pub fn get_authenticated_limiter(&self, account_id: u32) -> Arc> { + self.rate_limiter + .get(&account_id) + .map(|limiter| limiter.clone()) + .unwrap_or_else(|| { + let limiter = Arc::new(Mutex::new(AuthenticatedLimiter { + request_limiter: RateLimiter::new( + self.rate_requests.requests, + self.rate_requests.period, + ), + concurrent_requests: ConcurrencyLimiter::new(self.rate_concurrent), + concurrent_uploads: ConcurrencyLimiter::new(self.rate_concurrent), + })); + self.rate_limiter.insert(account_id, limiter.clone()); + limiter + }) + } +} diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index 6b15a5a1..dec809e8 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -14,6 +14,7 @@ use jmap_proto::{ use parking_lot::Mutex; use store::query::log::{Change, Query}; use tokio::io::AsyncRead; +use utils::listener::limiter::InFlight; use super::{Account, Mailbox, MailboxId, MailboxSync, Session, SessionData}; @@ -21,6 +22,7 @@ impl SessionData { pub async fn new( session: &Session, access_token: &AccessToken, + in_flight: InFlight, ) -> crate::Result { let mut session = SessionData { writer: session.writer.clone(), @@ -30,6 +32,7 @@ impl SessionData { span: session.span.clone(), mailboxes: Mutex::new(vec![]), state: access_token.state().into(), + in_flight, }; // Fetch mailboxes for the main account diff --git a/crates/imap/src/core/mod.rs b/crates/imap/src/core/mod.rs index e8e45ec2..c9171ade 100644 --- a/crates/imap/src/core/mod.rs +++ b/crates/imap/src/core/mod.rs @@ -5,20 +5,28 @@ use std::{ }; use ahash::AHashMap; +use dashmap::DashMap; use imap_proto::{ protocol::{list::Attribute, ProtocolVersion}, receiver::Receiver, Command, ResponseCode, StatusResponse, }; use jmap::{ - auth::{rate_limit::RemoteAddress, AccessToken}, + auth::{ + rate_limit::{AuthenticatedLimiter, RemoteAddress}, + AccessToken, + }, JMAP, }; +use parking_lot::Mutex; use tokio::{ io::{AsyncRead, ReadHalf}, sync::{mpsc, watch}, }; -use utils::listener::{limiter::InFlight, ServerInstance}; +use utils::{ + config::Rate, + listener::{limiter::InFlight, ServerInstance}, +}; pub mod client; pub mod mailbox; @@ -40,6 +48,7 @@ impl ImapSessionManager { pub struct IMAP { pub max_request_size: usize, + pub max_auth_failures: u32, pub name_shared: String, pub name_all: String, @@ -49,6 +58,10 @@ pub struct IMAP { pub greeting_plain: Vec, pub greeting_tls: Vec, + + pub rate_limiter: DashMap>>, + pub rate_requests: Rate, + pub rate_concurrent: u64, } pub struct Session { @@ -76,6 +89,7 @@ pub struct SessionData { pub mailboxes: parking_lot::Mutex>, pub writer: mpsc::Sender, pub state: AtomicU32, + pub in_flight: InFlight, } #[derive(Debug, Default)] diff --git a/crates/imap/src/core/session.rs b/crates/imap/src/core/session.rs index 567a1a53..42bf118b 100644 --- a/crates/imap/src/core/session.rs +++ b/crates/imap/src/core/session.rs @@ -103,7 +103,7 @@ impl Session { receiver: Receiver::with_max_request_size(manager.imap.max_request_size), version: ProtocolVersion::Rev1, state: State::NotAuthenticated { auth_failures: 0 }, - writer: writer::spawn_writer(writer::Event::Stream(stream_tx)), + writer: writer::spawn_writer(writer::Event::Stream(stream_tx), session.span.clone()), is_tls: false, is_condstore: false, is_qresync: false, @@ -191,7 +191,7 @@ impl Session> { receiver: Receiver::with_max_request_size(manager.imap.max_request_size), version: ProtocolVersion::Rev1, state: State::NotAuthenticated { auth_failures: 0 }, - writer: writer::spawn_writer(writer::Event::StreamTls(stream_tx)), + writer: writer::spawn_writer(writer::Event::StreamTls(stream_tx), span.clone()), is_tls: true, is_condstore: false, is_qresync: false, diff --git a/crates/imap/src/core/writer.rs b/crates/imap/src/core/writer.rs index 2b369088..14cb7298 100644 --- a/crates/imap/src/core/writer.rs +++ b/crates/imap/src/core/writer.rs @@ -42,7 +42,7 @@ pub enum Event { Upgrade(oneshot::Sender>), } -pub fn spawn_writer(mut stream: Event) -> mpsc::Sender { +pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender { let (tx, mut rx) = mpsc::channel::(IPC_CHANNEL_BUFFER); tokio::spawn(async move { 'outer: loop { @@ -51,6 +51,12 @@ pub fn spawn_writer(mut stream: Event) -> mpsc::Sender { while let Some(event) = rx.recv().await { match event { Event::Bytes(bytes) => { + tracing::trace!( + parent: &span, + event = "write", + data = std::str::from_utf8(bytes.as_ref()).unwrap_or_default(), + size = bytes.len() + ); /*let tmp = "dd"; println!( "<- {:?}", diff --git a/crates/imap/src/lib.rs b/crates/imap/src/lib.rs index 920d40d2..232cac29 100644 --- a/crates/imap/src/lib.rs +++ b/crates/imap/src/lib.rs @@ -1,7 +1,8 @@ -use std::sync::Arc; +use std::{collections::hash_map::RandomState, sync::Arc}; use crate::core::IMAP; +use dashmap::DashMap; use imap_proto::{protocol::capability::Capability, ResponseCode, StatusResponse}; use utils::config::Config; @@ -18,6 +19,7 @@ impl IMAP { pub async fn init(config: &Config) -> utils::config::Result> { Ok(Arc::new(IMAP { max_request_size: config.property_or_static("imap.request.max-size", "52428800")?, + max_auth_failures: config.property_or_static("imap.auth.max-failures", "3")?, name_shared: config .value("imap.folders.name.shared") .unwrap_or("Shared Folders") @@ -39,6 +41,16 @@ impl IMAP { capabilities: Capability::all_capabilities(false, true), }) .into_bytes(), + rate_limiter: DashMap::with_capacity_and_hasher_and_shard_amount( + config.property("imap.rate-limit.size")?.unwrap_or(2048), + RandomState::default(), + config + .property::("global.shared-map.shard")? + .unwrap_or(32) + .next_power_of_two() as usize, + ), + rate_requests: config.property_or_static("imap.rate-limit.rate", "1000/1m")?, + rate_concurrent: config.property("imap.rate-limit.concurrent")?.unwrap_or(4), })) } } diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs new file mode 100644 index 00000000..b25796c7 --- /dev/null +++ b/crates/imap/src/op/acl.rs @@ -0,0 +1,516 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::sync::Arc; + +use imap_proto::{ + protocol::acl::{ + Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights, + }, + receiver::Request, + Command, ResponseCode, StatusResponse, +}; + +use jmap::{ + auth::{acl::EffectiveAcl, AccessToken}, + mailbox::set::SCHEMA, +}; +use jmap_proto::{ + error::method::MethodError, + object::{index::ObjectIndexBuilder, Object}, + types::{ + acl::Acl, collection::Collection, id::Id, property::Property, state::StateChange, + type_state::TypeState, value::Value, + }, +}; +use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder}; +use tokio::io::AsyncRead; +use utils::map::bitmap::Bitmap; + +use crate::core::{MailboxId, Session, SessionData}; + +impl Session { + pub async fn handle_get_acl(&mut self, request: Request) -> crate::OpResult { + match request.parse_acl() { + Ok(arguments) => { + let data = self.state.session_data(); + let is_rev2 = self.version.is_rev2(); + + tokio::spawn(async move { + match data.get_acl_mailbox(&arguments, true).await { + Ok((_, values, _)) => { + let mut permissions = Vec::new(); + if let Some(acls) = values + .inner + .properties + .get(&Property::Acl) + .and_then(|v| v.as_list()) + { + for item in acls.chunks_exact(2) { + if let ( + Some(Value::Id(id)), + Some(Value::UnsignedInt(acl_bits)), + ) = (item.first(), item.last()) + { + if let Some(account_name) = data + .jmap + .directory + .principal_by_id(id.document_id()) + .await + .unwrap_or_default() + .and_then(|p| { + if p.has_name() { + Some(p.name().to_string()) + } else { + None + } + }) + { + let mut rights = Vec::new(); + + for acl in Bitmap::::from(*acl_bits) { + match acl { + Acl::Read => { + rights.push(Rights::Lookup); + } + Acl::Modify => { + rights.push(Rights::CreateMailbox); + } + Acl::Delete => { + rights.push(Rights::DeleteMailbox); + } + Acl::ReadItems => { + rights.push(Rights::Read); + } + Acl::AddItems => { + rights.push(Rights::Insert); + } + Acl::ModifyItems => { + rights.push(Rights::Write); + rights.push(Rights::Seen); + } + Acl::RemoveItems => { + rights.push(Rights::DeleteMessages); + rights.push(Rights::Expunge); + } + Acl::CreateChild => { + rights.push(Rights::CreateMailbox); + } + Acl::Administer => { + rights.push(Rights::Administer); + } + Acl::Submit => { + rights.push(Rights::Post); + } + Acl::None => (), + } + } + + permissions.push((account_name, rights)); + } + } + } + } + + data.write_bytes( + StatusResponse::completed(Command::GetAcl) + .with_tag(arguments.tag) + .serialize( + GetAclResponse { + mailbox_name: arguments.mailbox_name, + permissions, + } + .into_bytes(is_rev2), + ), + ) + .await; + } + Err(response) => { + data.write_bytes(response.with_tag(arguments.tag).into_bytes()) + .await; + return; + } + } + }); + Ok(()) + } + Err(response) => self.write_bytes(response.into_bytes()).await, + } + } + + pub async fn handle_my_rights(&mut self, request: Request) -> crate::OpResult { + match request.parse_acl() { + Ok(arguments) => { + let data = self.state.session_data(); + let is_rev2 = self.version.is_rev2(); + + tokio::spawn(async move { + match data.get_acl_mailbox(&arguments, false).await { + Ok((mailbox, values, access_token)) => { + data.write_bytes( + StatusResponse::completed(Command::MyRights) + .with_tag(arguments.tag) + .serialize( + MyRightsResponse { + mailbox_name: arguments.mailbox_name, + rights: if access_token.is_shared(mailbox.account_id) { + let acl = values.inner.effective_acl(&access_token); + let mut rights = Vec::with_capacity(5); + if acl.contains(Acl::ReadItems) { + rights.push(Rights::Read); + rights.push(Rights::Lookup); + } + if acl.contains(Acl::AddItems) { + rights.push(Rights::Insert); + } + if acl.contains(Acl::RemoveItems) { + rights.push(Rights::DeleteMessages); + rights.push(Rights::Expunge); + } + if acl.contains(Acl::ModifyItems) { + rights.push(Rights::Seen); + rights.push(Rights::Write); + } + if acl.contains(Acl::CreateChild) { + rights.push(Rights::CreateMailbox); + } + if acl.contains(Acl::Delete) { + rights.push(Rights::DeleteMailbox); + } + if acl.contains(Acl::Submit) { + rights.push(Rights::Post); + } + rights + } else { + vec![ + Rights::Read, + Rights::Lookup, + Rights::Insert, + Rights::DeleteMessages, + Rights::Expunge, + Rights::Seen, + Rights::Write, + Rights::CreateMailbox, + Rights::DeleteMailbox, + Rights::Post, + ] + }, + } + .into_bytes(is_rev2), + ), + ) + .await; + } + Err(response) => { + data.write_bytes(response.with_tag(arguments.tag).into_bytes()) + .await; + } + } + }); + Ok(()) + } + Err(response) => self.write_bytes(response.into_bytes()).await, + } + } + + pub async fn handle_set_acl(&mut self, request: Request) -> crate::OpResult { + let command = request.command; + match request.parse_acl() { + Ok(arguments) => { + let data = self.state.session_data(); + + tokio::spawn(async move { + // Validate mailbox + let (mailbox, values, _) = match data.get_acl_mailbox(&arguments, true).await { + Ok(result) => result, + Err(response) => { + data.write_bytes(response.with_tag(arguments.tag).into_bytes()) + .await; + return; + } + }; + + // Obtain principal id + let (acl_account_id, id) = match data + .jmap + .directory + .principal_by_name(arguments.identifier.as_ref().unwrap()) + .await + { + Ok(Some(principal)) if principal.has_id() => { + (principal.id(), Value::Id(Id::from(principal.id()))) + } + Ok(None) => { + data.write_bytes( + StatusResponse::no("Account does not exist") + .with_tag(arguments.tag) + .into_bytes(), + ) + .await; + return; + } + _ => { + data.write_bytes( + StatusResponse::database_failure() + .with_tag(arguments.tag) + .into_bytes(), + ) + .await; + return; + } + }; + + // Prepare changes + let mut changes = Object::with_capacity(1); + let (op, rights) = arguments + .mod_rights + .map(|mr| { + ( + mr.op, + Bitmap::from_iter(mr.rights.into_iter().map(Acl::from)), + ) + }) + .unwrap_or_else(|| (ModRightsOp::Replace, Bitmap::new())); + let acl = if let Value::List(acl) = + changes + .properties + .get_mut_or_insert_with(Property::Acl, || { + values + .inner + .properties + .get(&Property::Acl) + .cloned() + .unwrap_or_else(|| Value::List(Vec::new())) + }) { + acl + } else { + data.write_bytes( + StatusResponse::database_failure() + .with_tag(arguments.tag) + .into_bytes(), + ) + .await; + return; + }; + + if let Some(idx) = acl.iter().position(|item| item == &id) { + match op { + ModRightsOp::Replace => { + if !rights.is_empty() { + acl[idx + 1] = Value::UnsignedInt(rights.into()); + } else { + acl.remove(idx); + acl.remove(idx); + } + } + ModRightsOp::Add | ModRightsOp::Remove => { + if let Some(Value::UnsignedInt(current)) = acl.get_mut(idx + 1) { + let mut bitmap = Bitmap::from(*current); + if op == ModRightsOp::Add { + bitmap.union(&rights); + } else { + bitmap.intersection(&rights); + } + if !bitmap.is_empty() { + *current = bitmap.into(); + } else { + acl.remove(idx); + acl.remove(idx); + } + } else { + data.write_bytes( + StatusResponse::database_failure() + .with_tag(arguments.tag) + .into_bytes(), + ) + .await; + return; + } + } + } + } else if !rights.is_empty() { + match op { + ModRightsOp::Add | ModRightsOp::Replace => { + acl.push(id); + acl.push(Value::UnsignedInt(rights.into())); + } + ModRightsOp::Remove => (), + } + } + + // Write changes + let mailbox_id = mailbox.mailbox_id.unwrap(); + let mut batch = BatchBuilder::new(); + batch + .with_account_id(mailbox.account_id) + .with_collection(Collection::Mailbox) + .update_document(mailbox_id) + .custom( + ObjectIndexBuilder::new(SCHEMA) + .with_changes(changes) + .with_current(values), + ); + if !batch.is_empty() { + match data.jmap.write_batch(batch).await { + Ok(_) => { + let mut changes = ChangeLogBuilder::new(); + changes.log_update(Collection::Mailbox, mailbox_id); + match data.jmap.commit_changes(mailbox.account_id, changes).await { + Ok(change_id) => { + data.jmap + .broadcast_state_change( + StateChange::new(mailbox.account_id) + .with_change(TypeState::Mailbox, change_id), + ) + .await; + } + Err(_) => { + data.write_bytes( + StatusResponse::database_failure() + .with_tag(arguments.tag) + .into_bytes(), + ) + .await; + return; + } + } + } + Err(MethodError::ServerUnavailable) => { + data.write_bytes( + StatusResponse::no( + "Another process is currently updating this mailbox", + ) + .with_tag(arguments.tag) + .into_bytes(), + ) + .await; + return; + } + Err(_) => { + data.write_bytes( + StatusResponse::database_failure() + .with_tag(arguments.tag) + .into_bytes(), + ) + .await; + return; + } + } + } + + // Invalidate ACLs + data.jmap.access_tokens.remove(&acl_account_id); + + data.write_bytes( + StatusResponse::completed(command) + .with_tag(arguments.tag) + .into_bytes(), + ) + .await; + }); + Ok(()) + } + Err(response) => self.write_bytes(response.into_bytes()).await, + } + } + + pub async fn handle_list_rights(&mut self, request: Request) -> crate::OpResult { + match request.parse_acl() { + Ok(arguments) => { + self.write_bytes( + StatusResponse::completed(Command::ListRights) + .with_tag(arguments.tag) + .serialize( + ListRightsResponse { + mailbox_name: arguments.mailbox_name, + identifier: arguments.identifier.unwrap(), + permissions: vec![ + vec![Rights::Read], + vec![Rights::Lookup], + vec![Rights::Write, Rights::Seen], + vec![Rights::Insert], + vec![Rights::Expunge, Rights::DeleteMessages], + vec![Rights::CreateMailbox], + vec![Rights::DeleteMailbox], + vec![Rights::Post], + vec![Rights::Administer], + ], + } + .into_bytes(self.version.is_rev2()), + ), + ) + .await + } + Err(response) => self.write_bytes(response.into_bytes()).await, + } + } +} + +impl SessionData { + async fn get_acl_mailbox( + &self, + arguments: &Arguments, + validate: bool, + ) -> crate::op::Result<(MailboxId, HashedValue>, Arc)> { + if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) { + if let Some(mailbox_id) = mailbox.mailbox_id { + match ( + self.jmap + .get_property::>>( + mailbox.account_id, + Collection::Mailbox, + mailbox_id, + Property::Value, + ) + .await, + self.get_access_token().await, + ) { + (Ok(Some(values)), Ok(access_token)) => { + if !validate + || access_token.is_member(mailbox.account_id) + || values + .inner + .effective_acl(&access_token) + .contains(Acl::Administer) + { + Ok((mailbox, values, access_token)) + } else { + Err(StatusResponse::no( + "You do not have enough permissions to perform this operation.", + ) + .with_code(ResponseCode::NoPerm)) + } + } + (Ok(None), _) => Err(StatusResponse::no("Mailbox no longer exists.")), + _ => Err(StatusResponse::database_failure()), + } + } else { + Err(StatusResponse::no( + "ACL operations are not permitted on this mailbox.", + )) + } + } else { + Err(StatusResponse::no("Mailbox does not exist.")) + } + } +} diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs index a1b99a77..0c031411 100644 --- a/crates/imap/src/op/append.rs +++ b/crates/imap/src/op/append.rs @@ -35,7 +35,7 @@ use tokio::io::AsyncRead; use crate::core::{MailboxId, SelectedMailbox, Session, SessionData}; impl Session { - pub async fn handle_append(&mut self, request: Request) -> Result<(), ()> { + pub async fn handle_append(&mut self, request: Request) -> crate::OpResult { match request.parse_append() { Ok(arguments) => { let (data, selected_mailbox) = self.state.session_mailbox_state(); diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs index 99c2b224..ec598057 100644 --- a/crates/imap/src/op/authenticate.rs +++ b/crates/imap/src/op/authenticate.rs @@ -139,24 +139,43 @@ impl Session { }; if let Some(access_token) = access_token { - // Cache access token - let access_token = Arc::new(access_token); - self.jmap.cache_access_token(access_token.clone()); + // Enforce concurrency limits + let in_flight = self + .imap + .get_authenticated_limiter(access_token.primary_id()) + .lock() + .concurrent_requests + .is_allowed(); + if let Some(in_flight) = in_flight { + // Cache access token + let access_token = Arc::new(access_token); + self.jmap.cache_access_token(access_token.clone()); - // Create session - self.state = State::Authenticated { - data: Arc::new(SessionData::new(self, &access_token).await?), - }; - self.write_bytes( - StatusResponse::ok("Authentication successful") - .with_code(ResponseCode::Capability { - capabilities: Capability::all_capabilities(true, self.is_tls), - }) - .with_tag(tag) - .into_bytes(), - ) - .await?; - Ok(()) + // Create session + self.state = State::Authenticated { + data: Arc::new(SessionData::new(self, &access_token, in_flight).await?), + }; + self.write_bytes( + StatusResponse::ok("Authentication successful") + .with_code(ResponseCode::Capability { + capabilities: Capability::all_capabilities(true, self.is_tls), + }) + .with_tag(tag) + .into_bytes(), + ) + .await?; + Ok(()) + } else { + self.write_bytes( + StatusResponse::bye("Too many concurrent IMAP connections.").into_bytes(), + ) + .await?; + tracing::debug!(parent: &self.span, + event = "disconnect", + "Too many concurrent connections, disconnecting.", + ); + return Err(()); + } } else { self.write_bytes( StatusResponse::no("Authentication failed") @@ -167,7 +186,7 @@ impl Session { .await?; let auth_failures = self.state.auth_failures(); - if auth_failures < 3 { + if auth_failures < self.imap.max_auth_failures { self.state = State::NotAuthenticated { auth_failures: auth_failures + 1, }; diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs new file mode 100644 index 00000000..21ee3827 --- /dev/null +++ b/crates/imap/src/op/capability.rs @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::{ + protocol::{ + capability::{Capability, Response}, + ImapResponse, + }, + receiver::Request, + Command, StatusResponse, +}; + +use tokio::io::AsyncRead; + +use crate::core::Session; + +impl Session { + pub async fn handle_capability(&mut self, request: Request) -> crate::OpResult { + self.write_bytes( + StatusResponse::completed(Command::Capability) + .with_tag(request.tag) + .serialize( + Response { + capabilities: Capability::all_capabilities( + self.state.is_authenticated(), + self.is_tls, + ), + } + .serialize(), + ), + ) + .await + } + + pub async fn handle_id(&mut self, request: Request) -> crate::OpResult { + self.write_bytes( + StatusResponse::completed(Command::Id) + .with_tag(request.tag) + .serialize( + concat!( + "* ID (\"name\" \"Stalwart IMAP\" \"version\" \"", + env!("CARGO_PKG_VERSION"), + "\" \"vendor\" \"Stalwart Labs Ltd.\" ", + "\"support-url\" \"https://stalw.art/imap\")\r\n" + ) + .as_bytes() + .to_vec(), + ), + ) + .await + } +} diff --git a/crates/imap/src/op/idle.rs b/crates/imap/src/op/idle.rs index a2b0d351..487262fe 100644 --- a/crates/imap/src/op/idle.rs +++ b/crates/imap/src/op/idle.rs @@ -45,7 +45,7 @@ use crate::core::{SelectedMailbox, Session, SessionData, State}; impl Session { pub async fn handle_idle(&mut self, request: Request) -> crate::OpResult { let (data, mailbox, types) = match &self.state { - State::Authenticated { data } => { + State::Authenticated { data, .. } => { (data.clone(), None, Bitmap::from_iter([TypeState::Mailbox])) } State::Selected { data, mailbox, .. } => ( diff --git a/crates/imap/src/op/login.rs b/crates/imap/src/op/login.rs index 9a62a7bb..52c69ac8 100644 --- a/crates/imap/src/op/login.rs +++ b/crates/imap/src/op/login.rs @@ -29,7 +29,7 @@ use tokio::io::AsyncRead; use crate::core::Session; impl Session { - pub async fn handle_login(&mut self, request: Request) -> Result<(), ()> { + pub async fn handle_login(&mut self, request: Request) -> crate::OpResult { match request.parse_login() { Ok(args) => { self.authenticate( diff --git a/crates/imap/src/op/mod.rs b/crates/imap/src/op/mod.rs index 519b22c7..74caee94 100644 --- a/crates/imap/src/op/mod.rs +++ b/crates/imap/src/op/mod.rs @@ -1,8 +1,10 @@ use ::store::query::log::Query; use imap_proto::StatusResponse; +pub mod acl; pub mod append; pub mod authenticate; +pub mod capability; pub mod close; pub mod copy_move; pub mod create; diff --git a/crates/imap/src/op/noop.rs b/crates/imap/src/op/noop.rs index 67e83219..82a77bb4 100644 --- a/crates/imap/src/op/noop.rs +++ b/crates/imap/src/op/noop.rs @@ -28,13 +28,9 @@ use tokio::io::AsyncRead; use crate::core::{Session, State}; impl Session { - pub async fn handle_noop( - &mut self, - request: Request, - is_check: bool, - ) -> Result<(), ()> { + pub async fn handle_noop(&mut self, request: Request) -> crate::OpResult { match &self.state { - State::Authenticated { data } => { + State::Authenticated { data, .. } => { data.write_changes(&None, true, false, self.is_qresync, self.version.is_rev2()) .await; } @@ -52,13 +48,9 @@ impl Session { } self.write_bytes( - StatusResponse::completed(if !is_check { - Command::Noop - } else { - Command::Check - }) - .with_tag(request.tag) - .into_bytes(), + StatusResponse::completed(request.command) + .with_tag(request.tag) + .into_bytes(), ) .await } diff --git a/crates/imap/src/op/search.rs b/crates/imap/src/op/search.rs index 262a6279..7285ba7a 100644 --- a/crates/imap/src/op/search.rs +++ b/crates/imap/src/op/search.rs @@ -52,7 +52,7 @@ impl Session { request: Request, is_sort: bool, is_uid: bool, - ) -> Result<(), ()> { + ) -> crate::OpResult { match if !is_sort { request.parse_search(self.version) } else { diff --git a/crates/imap/src/op/select.rs b/crates/imap/src/op/select.rs index aca4691d..83c93285 100644 --- a/crates/imap/src/op/select.rs +++ b/crates/imap/src/op/select.rs @@ -159,7 +159,7 @@ impl Session { } } - pub async fn handle_unselect(&mut self, request: Request) -> Result<(), ()> { + pub async fn handle_unselect(&mut self, request: Request) -> crate::OpResult { self.state = State::Authenticated { data: self.state.session_data(), }; diff --git a/crates/imap/src/op/thread.rs b/crates/imap/src/op/thread.rs index 13842695..9b27e9b5 100644 --- a/crates/imap/src/op/thread.rs +++ b/crates/imap/src/op/thread.rs @@ -42,16 +42,16 @@ use crate::core::{SelectedMailbox, Session, SessionData}; impl Session { pub async fn handle_thread( &mut self, - mut request: Request, + request: Request, is_uid: bool, - ) -> Result<(), ()> { + ) -> crate::OpResult { let command = request.command; - let tag = std::mem::take(&mut request.tag); match request.parse_thread() { - Ok(arguments) => { + Ok(mut arguments) => { let (data, mailbox) = self.state.mailbox_state(); tokio::spawn(async move { + let tag = std::mem::take(&mut arguments.tag); let bytes = match data.thread(arguments, mailbox, is_uid).await { Ok(response) => StatusResponse::completed(command) .with_tag(tag) diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 5b82c54a..a8f42560 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -38,6 +38,7 @@ reqwest = { version = "0.11", default-features = false, features = ["rustls-tls- tokio-tungstenite = "0.19.0" tungstenite = "0.19.0" chrono = "0.4" +dashmap = "5.4" [dev-dependencies] ece = "2.2" diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index a5a6266b..e266a5cf 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -74,7 +74,7 @@ struct Account { #[derive(Debug, Clone, serde::Serialize)] #[serde(untagged)] #[allow(dead_code)] -enum Capabilities { +pub enum Capabilities { Core(CoreCapabilities), Mail(MailCapabilities), Submission(SubmissionCapabilities), @@ -84,7 +84,7 @@ enum Capabilities { } #[derive(Debug, Clone, serde::Serialize)] -struct CoreCapabilities { +pub struct CoreCapabilities { #[serde(rename(serialize = "maxSizeUpload"))] max_size_upload: usize, #[serde(rename(serialize = "maxConcurrentUpload"))] @@ -104,7 +104,7 @@ struct CoreCapabilities { } #[derive(Debug, Clone, serde::Serialize)] -struct WebSocketCapabilities { +pub struct WebSocketCapabilities { #[serde(rename(serialize = "url"))] url: String, #[serde(rename(serialize = "supportsPush"))] @@ -112,27 +112,27 @@ struct WebSocketCapabilities { } #[derive(Debug, Clone, serde::Serialize)] -struct SieveCapabilities { +pub struct SieveCapabilities { #[serde(rename(serialize = "implementation"))] - implementation: &'static str, + pub implementation: &'static str, #[serde(rename(serialize = "maxSizeScriptName"))] - max_script_name: usize, + pub max_script_name: usize, #[serde(rename(serialize = "maxSizeScript"))] - max_script_size: usize, + pub max_script_size: usize, #[serde(rename(serialize = "maxNumberScripts"))] - max_scripts: usize, + pub max_scripts: usize, #[serde(rename(serialize = "maxNumberRedirects"))] - max_redirects: usize, + pub max_redirects: usize, #[serde(rename(serialize = "sieveExtensions"))] - extensions: Vec, + pub extensions: Vec, #[serde(rename(serialize = "notificationMethods"))] - notification_methods: Option>, + pub notification_methods: Option>, #[serde(rename(serialize = "externalLists"))] - ext_lists: Option>, + pub ext_lists: Option>, } #[derive(Debug, Clone, serde::Serialize)] -struct MailCapabilities { +pub struct MailCapabilities { #[serde(rename(serialize = "maxMailboxesPerEmail"))] max_mailboxes_per_email: Option, #[serde(rename(serialize = "maxMailboxDepth"))] @@ -148,7 +148,7 @@ struct MailCapabilities { } #[derive(Debug, Clone, serde::Serialize)] -struct SubmissionCapabilities { +pub struct SubmissionCapabilities { #[serde(rename(serialize = "maxDelayedSend"))] max_delayed_send: usize, #[serde(rename(serialize = "submissionExtensions"))] @@ -156,11 +156,11 @@ struct SubmissionCapabilities { } #[derive(Debug, Clone, serde::Serialize)] -struct VacationResponseCapabilities {} +pub struct VacationResponseCapabilities {} #[derive(Default)] pub struct BaseCapabilities { - capabilities: VecMap, + pub capabilities: VecMap, } impl JMAP { diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index 3e5e9311..9057c240 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -454,7 +454,7 @@ impl JMAP { current: &Option>>, ) { if let Value::List(acl_changes) = changes.get(&Property::Acl) { - let mut access_tokens = self.access_tokens.lock(); + let access_tokens = &self.access_tokens; if let Some(Value::List(acl_current)) = current .as_ref() .and_then(|current| current.inner.properties.get(&Property::Acl)) diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs index 4f05b1b0..31ad6dd1 100644 --- a/crates/jmap/src/auth/authenticate.rs +++ b/crates/jmap/src/auth/authenticate.rs @@ -30,8 +30,8 @@ use std::{ use hyper::header; use jmap_proto::error::request::RequestError; use mail_parser::decoders::base64::base64_decode; -use mail_send::{mail_auth::common::lru::DnsCache, Credentials}; -use utils::listener::limiter::InFlight; +use mail_send::Credentials; +use utils::{listener::limiter::InFlight, map::ttl_dashmap::TtlMap}; use crate::JMAP; @@ -49,7 +49,7 @@ impl JMAP { .and_then(|h| h.to_str().ok()) .and_then(|h| h.split_once(' ').map(|(l, t)| (l, t.trim().to_string()))) { - let session = if let Some(account_id) = self.sessions.get(&token) { + let session = if let Some(account_id) = self.sessions.get_with_ttl(&token) { self.get_cached_access_token(account_id).await } else { let addr = self.build_remote_addr(req, remote_ip); @@ -118,7 +118,7 @@ impl JMAP { } pub fn cache_session(&self, session_id: String, access_token: &AccessToken) { - self.sessions.insert( + self.sessions.insert_with_ttl( session_id, access_token.primary_id(), Instant::now() + self.config.session_cache_ttl, @@ -126,7 +126,7 @@ impl JMAP { } pub fn cache_access_token(&self, access_token: Arc) { - self.access_tokens.insert( + self.access_tokens.insert_with_ttl( access_token.primary_id(), access_token, Instant::now() + self.config.session_cache_ttl, @@ -134,7 +134,7 @@ impl JMAP { } pub async fn get_cached_access_token(&self, primary_id: u32) -> Option> { - if let Some(access_token) = self.access_tokens.get(&primary_id) { + if let Some(access_token) = self.access_tokens.get_with_ttl(&primary_id) { access_token.into() } else { // Refresh ACL token diff --git a/crates/jmap/src/auth/oauth/device_auth.rs b/crates/jmap/src/auth/oauth/device_auth.rs index e87b6c68..98001cbc 100644 --- a/crates/jmap/src/auth/oauth/device_auth.rs +++ b/crates/jmap/src/auth/oauth/device_auth.rs @@ -27,12 +27,11 @@ use std::{ }; use hyper::StatusCode; -use mail_send::mail_auth::common::lru::DnsCache; use store::rand::{ distributions::{Alphanumeric, Standard}, thread_rng, Rng, }; -use utils::listener::ServerInstance; +use utils::{listener::ServerInstance, map::ttl_dashmap::TtlMap}; use crate::{ api::{http::ToHttpResponse, HtmlResponse, HttpRequest, HttpResponse, JsonResponse}, @@ -102,9 +101,9 @@ impl JMAP { }); let expiry = Instant::now() + Duration::from_secs(self.config.oauth_expiry_user_code); self.oauth_codes - .insert(device_code.clone(), oauth_code.clone(), expiry); + .insert_with_ttl(device_code.clone(), oauth_code.clone(), expiry); self.oauth_codes - .insert(user_code.clone(), oauth_code, expiry); + .insert_with_ttl(user_code.clone(), oauth_code, expiry); // Build response JsonResponse::new(DeviceAuthResponse { @@ -164,7 +163,7 @@ impl JMAP { let code = if let Some(oauth) = fields .get("code") - .and_then(|code| self.oauth_codes.get(code)) + .and_then(|code| self.oauth_codes.get_with_ttl(code)) { if (STATUS_PENDING..STATUS_PENDING + self.config.oauth_max_auth_attempts) .contains(&oauth.status.load(atomic::Ordering::Relaxed)) diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index 271cb5e9..121c9257 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -26,12 +26,14 @@ use std::{sync::atomic, time::SystemTime}; use hyper::StatusCode; use mail_builder::encoders::base64::base64_encode; use mail_parser::decoders::base64::base64_decode; -use mail_send::mail_auth::common::lru::DnsCache; use store::{ blake3, rand::{thread_rng, Rng}, }; -use utils::codec::leb128::{Leb128Iterator, Leb128Vec}; +use utils::{ + codec::leb128::{Leb128Iterator, Leb128Vec}, + map::ttl_dashmap::TtlMap, +}; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, @@ -65,7 +67,7 @@ impl JMAP { params.get("client_id"), params.get("redirect_uri"), ) { - if let Some(oauth) = self.oauth_codes.get(code) { + if let Some(oauth) = self.oauth_codes.get_with_ttl(code) { if client_id != &oauth.client_id || redirect_uri != oauth.redirect_uri.as_deref().unwrap_or("") { @@ -102,7 +104,7 @@ impl JMAP { if let (Some(oauth), Some(client_id)) = ( params .get("device_code") - .and_then(|dc| self.oauth_codes.get(dc)), + .and_then(|dc| self.oauth_codes.get_with_ttl(dc)), params.get("client_id"), ) { response = if &oauth.client_id != client_id { diff --git a/crates/jmap/src/auth/oauth/user_code.rs b/crates/jmap/src/auth/oauth/user_code.rs index e55a9fb8..e449abe8 100644 --- a/crates/jmap/src/auth/oauth/user_code.rs +++ b/crates/jmap/src/auth/oauth/user_code.rs @@ -31,9 +31,9 @@ use http_body_util::{BodyExt, Full}; use hyper::{body::Bytes, header, StatusCode}; use mail_builder::encoders::base64::base64_encode; use mail_parser::decoders::base64::base64_decode; -use mail_send::mail_auth::common::lru::DnsCache; use std::fmt::Write; use store::rand::{distributions::Alphanumeric, thread_rng, Rng}; +use utils::map::ttl_dashmap::TtlMap; use crate::{ api::{http::ToHttpResponse, HtmlResponse, HttpRequest, HttpResponse}, @@ -141,7 +141,7 @@ impl JMAP { .collect::(); // Add client code - self.oauth_codes.insert( + self.oauth_codes.insert_with_ttl( client_code.clone(), Arc::new(OAuthCode { status: STATUS_AUTHORIZED.into(), diff --git a/crates/jmap/src/auth/rate_limit.rs b/crates/jmap/src/auth/rate_limit.rs index afc6e444..f03c9473 100644 --- a/crates/jmap/src/auth/rate_limit.rs +++ b/crates/jmap/src/auth/rate_limit.rs @@ -21,14 +21,9 @@ * for more details. */ -use std::{ - net::IpAddr, - sync::Arc, - time::{Duration, Instant}, -}; +use std::{net::IpAddr, sync::Arc}; use jmap_proto::error::request::{RequestError, RequestLimitError}; -use mail_send::mail_auth::common::lru::DnsCache; use store::parking_lot::Mutex; use utils::listener::limiter::{ConcurrencyLimiter, InFlight, RateLimiter}; @@ -43,9 +38,9 @@ pub enum RemoteAddress { } pub struct AuthenticatedLimiter { - request_limiter: RateLimiter, - concurrent_requests: ConcurrencyLimiter, - concurrent_uploads: ConcurrencyLimiter, + pub request_limiter: RateLimiter, + pub concurrent_requests: ConcurrencyLimiter, + pub concurrent_uploads: ConcurrencyLimiter, } #[derive(Debug)] @@ -56,45 +51,45 @@ pub struct AnonymousLimiter { impl JMAP { pub fn get_authenticated_limiter(&self, account_id: u32) -> Arc> { - self.rate_limit_auth.get(&account_id).unwrap_or_else(|| { - let limiter = Arc::new(Mutex::new(AuthenticatedLimiter { - request_limiter: RateLimiter::new( - self.config.rate_authenticated.requests, - self.config.rate_authenticated.period, - ), - concurrent_requests: ConcurrencyLimiter::new(self.config.request_max_concurrent), - concurrent_uploads: ConcurrencyLimiter::new( - self.config.upload_max_concurrent as u64, - ), - })); - self.rate_limit_auth.insert( - account_id, - limiter.clone(), - Instant::now() + self.config.session_cache_ttl, - ); - limiter - }) + self.rate_limit_auth + .get(&account_id) + .map(|limiter| limiter.clone()) + .unwrap_or_else(|| { + let limiter = Arc::new(Mutex::new(AuthenticatedLimiter { + request_limiter: RateLimiter::new( + self.config.rate_authenticated.requests, + self.config.rate_authenticated.period, + ), + concurrent_requests: ConcurrencyLimiter::new( + self.config.request_max_concurrent, + ), + concurrent_uploads: ConcurrencyLimiter::new( + self.config.upload_max_concurrent as u64, + ), + })); + self.rate_limit_auth.insert(account_id, limiter.clone()); + limiter + }) } pub fn get_anonymous_limiter(&self, addr: RemoteAddress) -> Arc> { - self.rate_limit_unauth.get(&addr).unwrap_or_else(|| { - let limiter = Arc::new(Mutex::new(AnonymousLimiter { - request_limiter: RateLimiter::new( - self.config.rate_anonymous.requests, - self.config.rate_anonymous.period, - ), - auth_limiter: RateLimiter::new( - self.config.rate_authenticate_req.requests, - self.config.rate_authenticate_req.period, - ), - })); - self.rate_limit_unauth.insert( - addr, - limiter.clone(), - Instant::now() + Duration::from_secs(86400), - ); - limiter - }) + self.rate_limit_unauth + .get(&addr) + .map(|limiter| limiter.clone()) + .unwrap_or_else(|| { + let limiter = Arc::new(Mutex::new(AnonymousLimiter { + request_limiter: RateLimiter::new( + self.config.rate_anonymous.requests, + self.config.rate_anonymous.period, + ), + auth_limiter: RateLimiter::new( + self.config.rate_authenticate_req.requests, + self.config.rate_authenticate_req.period, + ), + })); + self.rate_limit_unauth.insert(addr, limiter.clone()); + limiter + }) } pub fn is_account_allowed(&self, access_token: &AccessToken) -> Result { @@ -157,3 +152,17 @@ impl JMAP { } } } + +impl AuthenticatedLimiter { + pub fn is_active(&self) -> bool { + self.request_limiter.is_active() + || self.concurrent_requests.is_active() + || self.concurrent_uploads.is_active() + } +} + +impl AnonymousLimiter { + pub fn is_active(&self) -> bool { + self.request_limiter.is_active() || self.auth_limiter.is_active() + } +} diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index 8edff8c2..399daadd 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -21,7 +21,7 @@ * for more details. */ -use std::{sync::Arc, time::Duration}; +use std::{collections::hash_map::RandomState, sync::Arc, time::Duration}; use ::sieve::{Compiler, Runtime}; use api::session::BaseCapabilities; @@ -30,6 +30,7 @@ use auth::{ rate_limit::{AnonymousLimiter, AuthenticatedLimiter, RemoteAddress}, AccessToken, }; +use dashmap::DashMap; use directory::{Directory, DirectoryConfig}; use jmap_proto::{ error::method::MethodError, @@ -39,7 +40,6 @@ use jmap_proto::{ }, types::{collection::Collection, property::Property}, }; -use mail_send::mail_auth::common::lru::{DnsCache, LruCache}; use services::{ delivery::spawn_delivery_manager, housekeeper::{self, init_housekeeper, spawn_housekeeper}, @@ -55,7 +55,12 @@ use store::{ BitmapKey, Deserialize, Serialize, Store, ValueKey, }; use tokio::sync::mpsc; -use utils::{config::Rate, ipc::DeliveryEvent, UnwrapFailure}; +use utils::{ + config::Rate, + ipc::DeliveryEvent, + map::ttl_dashmap::{TtlDashMap, TtlMap}, + UnwrapFailure, +}; pub mod api; pub mod auth; @@ -80,13 +85,13 @@ pub struct JMAP { pub config: Config, pub directory: Arc, - pub sessions: LruCache, - pub access_tokens: LruCache>, + pub sessions: TtlDashMap, + pub access_tokens: TtlDashMap>, - pub rate_limit_auth: LruCache>>, - pub rate_limit_unauth: LruCache>>, + pub rate_limit_auth: DashMap>>, + pub rate_limit_unauth: DashMap>>, - pub oauth_codes: LruCache>, + pub oauth_codes: TtlDashMap>, pub state_tx: mpsc::Sender, pub housekeeper_tx: mpsc::Sender, @@ -168,6 +173,10 @@ impl JMAP { // Init state manager and housekeeper let (state_tx, state_rx) = init_state_manager(); let (housekeeper_tx, housekeeper_rx) = init_housekeeper(); + let shard_amount = config + .property::("global.shared-map.shard")? + .unwrap_or(32) + .next_power_of_two() as usize; let jmap_server = Arc::new(JMAP { directory: directory_config @@ -180,24 +189,31 @@ impl JMAP { .clone(), store: Store::open(config).await.failed("Unable to open database"), config: Config::new(config).failed("Invalid configuration file"), - sessions: LruCache::with_capacity( + sessions: TtlDashMap::with_capacity( config.property("jmap.session.cache.size")?.unwrap_or(100), + shard_amount, ), - access_tokens: LruCache::with_capacity( + access_tokens: TtlDashMap::with_capacity( config.property("jmap.session.cache.size")?.unwrap_or(100), + shard_amount, ), - rate_limit_auth: LruCache::with_capacity( + rate_limit_auth: DashMap::with_capacity_and_hasher_and_shard_amount( config .property("jmap.rate-limit.account.size")? .unwrap_or(1024), + RandomState::default(), + shard_amount, ), - rate_limit_unauth: LruCache::with_capacity( + rate_limit_unauth: DashMap::with_capacity_and_hasher_and_shard_amount( config .property("jmap.rate-limit.anonymous.size")? .unwrap_or(2048), + RandomState::default(), + shard_amount, ), - oauth_codes: LruCache::with_capacity( + oauth_codes: TtlDashMap::with_capacity( config.property("oauth.code.cache-size")?.unwrap_or(128), + shard_amount, ), state_tx, housekeeper_tx, diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index 422b537e..7c23fe01 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -186,9 +186,10 @@ impl JMAP { .update_document(document_id) .custom(builder); if !batch.is_empty() { - changes.log_update(Collection::Mailbox, document_id); match self.store.write(batch.build()).await { - Ok(_) => (), + Ok(_) => { + changes.log_update(Collection::Mailbox, document_id); + } Err(store::Error::AssertValueFailed) => { ctx.response.not_updated.append(id, SetError::forbidden().with_description( "Another process modified this mailbox, please try again.", diff --git a/crates/jmap/src/services/housekeeper.rs b/crates/jmap/src/services/housekeeper.rs index a77fe97f..1b37dda2 100644 --- a/crates/jmap/src/services/housekeeper.rs +++ b/crates/jmap/src/services/housekeeper.rs @@ -26,7 +26,7 @@ use std::{sync::Arc, time::Duration}; use chrono::{Datelike, TimeZone}; use store::write::now; use tokio::sync::mpsc; -use utils::{config::Config, failed, UnwrapFailure}; +use utils::{config::Config, failed, map::ttl_dashmap::TtlMap, UnwrapFailure}; use crate::JMAP; @@ -35,6 +35,7 @@ use super::IPC_CHANNEL_BUFFER; pub enum Event { PurgeDb, PurgeBlobs, + PurgeCache, Exit, } @@ -45,6 +46,7 @@ enum SimpleCron { const TASK_PURGE_DB: usize = 0; const TASK_PURGE_BLOBS: usize = 1; +const TASK_PURGE_CACHE: usize = 2; pub fn spawn_housekeeper(core: Arc, settings: &Config, mut rx: mpsc::Receiver) { let purge_db_at = SimpleCron::parse( @@ -57,12 +59,21 @@ pub fn spawn_housekeeper(core: Arc, settings: &Config, mut rx: mpsc::Recei .value("jmap.house-keeper.purge-blobs") .unwrap_or("30 3 *"), ); + let purge_cache = SimpleCron::parse( + settings + .value("jmap.house-keeper.purge-cache") + .unwrap_or("15 * *"), + ); tokio::spawn(async move { tracing::debug!("Housekeeper task started."); loop { - let time_to_next = [purge_db_at.time_to_next(), purge_blobs_at.time_to_next()]; - let mut tasks_to_run = [false, false]; + let time_to_next = [ + purge_db_at.time_to_next(), + purge_blobs_at.time_to_next(), + purge_cache.time_to_next(), + ]; + let mut tasks_to_run = [false, false, false]; let start_time = now(); match tokio::time::timeout(time_to_next.iter().min().copied().unwrap(), rx.recv()).await @@ -70,6 +81,7 @@ pub fn spawn_housekeeper(core: Arc, settings: &Config, mut rx: mpsc::Recei Ok(Some(event)) => match event { Event::PurgeDb => tasks_to_run[TASK_PURGE_DB] = true, Event::PurgeBlobs => tasks_to_run[TASK_PURGE_BLOBS] = true, + Event::PurgeCache => tasks_to_run[TASK_PURGE_CACHE] = true, Event::Exit => { tracing::debug!("Housekeeper task exiting."); return; @@ -114,6 +126,16 @@ pub fn spawn_housekeeper(core: Arc, settings: &Config, mut rx: mpsc::Recei tracing::error!("Error while purging bitmaps: {}", err); } } + TASK_PURGE_CACHE => { + tracing::info!("Purging session cache."); + core.sessions.cleanup(); + core.access_tokens.cleanup(); + core.oauth_codes.cleanup(); + core.rate_limit_auth + .retain(|_, limiter| limiter.lock().is_active()); + core.rate_limit_unauth + .retain(|_, limiter| limiter.lock().is_active()); + } _ => unreachable!(), } }); diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index 276d1651..bf56fdca 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -247,11 +247,6 @@ impl JMAP { } } - // Write changes - if !changes.is_empty() { - ctx.response.new_state = Some(self.commit_changes(account_id, changes).await?.into()); - } - // Activate / deactivate scripts if ctx.response.not_created.is_empty() && ctx.response.not_updated.is_empty() @@ -283,9 +278,15 @@ impl JMAP { if let Some(obj) = ctx.response.get_object_by_id(Id::from(document_id)) { obj.append(Property::IsActive, Value::Bool(is_active)); } + changes.log_update(Collection::SieveScript, document_id); } } + // Write changes + if !changes.is_empty() { + ctx.response.new_state = Some(self.commit_changes(account_id, changes).await?.into()); + } + Ok(ctx.response) } diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index 0171c96d..ae0f55c0 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -24,7 +24,9 @@ use std::time::Duration; use directory::config::ConfigDirectory; +use imap::core::{ImapSessionManager, IMAP}; use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP}; +use managesieve::core::ManageSieveSessionManager; use smtp::core::{SmtpAdminSessionManager, SmtpSessionManager, SMTP}; use tokio::sync::mpsc; use utils::{ @@ -54,6 +56,7 @@ async fn main() -> std::io::Result<()> { "Starting Stalwart Mail Server v{}...", env!("CARGO_PKG_VERSION") ); + let todo = "fix logging"; // Init servers let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER); @@ -63,6 +66,9 @@ async fn main() -> std::io::Result<()> { let jmap = JMAP::init(&config, &directory, delivery_rx, smtp.clone()) .await .failed("Invalid configuration file"); + let imap = IMAP::init(&config) + .await + .failed("Invalid configuration file"); // Spawn servers let shutdown_tx = servers.spawn(|server, shutdown_rx| { @@ -76,7 +82,14 @@ async fn main() -> std::io::Result<()> { ServerProtocol::Jmap => { server.spawn(JmapSessionManager::new(jmap.clone()), shutdown_rx) } - ServerProtocol::Imap => unimplemented!("IMAP is not implemented yet"), + ServerProtocol::Imap => server.spawn( + ImapSessionManager::new(jmap.clone(), imap.clone()), + shutdown_rx, + ), + ServerProtocol::ManageSieve => server.spawn( + ManageSieveSessionManager::new(jmap.clone(), imap.clone()), + shutdown_rx, + ), }; }); diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml new file mode 100644 index 00000000..fc15c133 --- /dev/null +++ b/crates/managesieve/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "managesieve" +version = "0.1.0" +edition = "2021" +resolver = "2" + +[dependencies] +imap_proto = { path = "../imap-proto" } +imap = { path = "../imap" } +jmap = { path = "../jmap" } +jmap_proto = { path = "../jmap-proto" } +directory = { path = "../directory" } +store = { path = "../store" } +utils = { path = "../utils" } +mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "ludicrous_mode"] } +mail-send = { git = "https://github.com/stalwartlabs/mail-send", default-features = false, features = ["cram-md5", "skip-ehlo"] } +sieve-rs = { git = "https://github.com/stalwartlabs/sieve" } +rustls = "0.21.0" +rustls-pemfile = "1.0" +tokio = { version = "1.23", features = ["full"] } +tokio-rustls = { version = "0.24.0"} +parking_lot = "0.12" +tracing = "0.1" +ahash = { version = "0.8" } +md5 = "0.7.0" +bincode = "1.3.3" + + +[features] +test_mode = [] diff --git a/crates/managesieve/src/core/client.rs b/crates/managesieve/src/core/client.rs new file mode 100644 index 00000000..cfada9f6 --- /dev/null +++ b/crates/managesieve/src/core/client.rs @@ -0,0 +1,211 @@ +use imap::core::IMAP; +use imap_proto::receiver::{self, Request}; +use jmap_proto::types::{collection::Collection, property::Property}; +use store::query::Filter; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +use super::{Command, IsTls, ResponseCode, ResponseType, Session, State, StatusResponse}; + +impl Session { + pub async fn ingest(&mut self, bytes: &[u8]) -> Result { + /*let tmp = "dd"; + for line in String::from_utf8_lossy(bytes).split("\r\n") { + println!("<- {:?}", &line[..std::cmp::min(line.len(), 100)]); + }*/ + + let mut bytes = bytes.iter(); + let mut requests = Vec::with_capacity(2); + let mut needs_literal = None; + + loop { + match self.receiver.parse(&mut bytes) { + Ok(request) => { + match request.is_allowed(&self.imap, &self.state, self.stream.is_tls()) { + Ok(request) => { + requests.push(request); + } + Err(response) => { + self.write(&response.into_bytes()).await?; + } + } + } + Err(receiver::Error::NeedsMoreData) => { + break; + } + Err(receiver::Error::NeedsLiteral { size }) => { + needs_literal = size.into(); + break; + } + Err(receiver::Error::Error { response }) => { + self.write(&StatusResponse::no(response.message).into_bytes()) + .await?; + break; + } + } + } + + for request in requests { + match match request.command { + Command::ListScripts => self.handle_listscripts().await, + Command::PutScript => self.handle_putscript(request).await, + Command::SetActive => self.handle_setactive(request).await, + Command::GetScript => self.handle_getscript(request).await, + Command::DeleteScript => self.handle_deletescript(request).await, + Command::RenameScript => self.handle_renamescript(request).await, + Command::CheckScript => self.handle_checkscript(request).await, + Command::HaveSpace => self.handle_havespace(request).await, + Command::Capability => self.handle_capability("").await, + Command::Authenticate => self.handle_authenticate(request).await, + Command::StartTls => { + self.write(b"OK Begin TLS negotiation now\r\n").await?; + return Ok(true); + } + Command::Logout => self.handle_logout().await, + Command::Noop => self.handle_noop(request).await, + Command::Unauthenticate => self.handle_unauthenticate().await, + } { + Ok(response) => { + self.write(&response).await?; + } + Err(err) => { + let disconnect = err.rtype == ResponseType::Bye; + self.write(&err.into_bytes()).await?; + if disconnect { + return Err(()); + } + } + } + } + + if let Some(needs_literal) = needs_literal { + self.write(format!("OK Ready for {} bytes.\r\n", needs_literal).as_bytes()) + .await?; + } + + Ok(false) + } +} + +impl Session { + #[inline(always)] + pub async fn write(&mut self, bytes: &[u8]) -> Result<(), ()> { + let err = match self.stream.write_all(bytes).await { + Ok(_) => match self.stream.flush().await { + Ok(_) => { + tracing::trace!(parent: &self.span, + event = "write", + data = std::str::from_utf8(bytes).unwrap_or_default() , + size = bytes.len()); + return Ok(()); + } + Err(err) => err, + }, + Err(err) => err, + }; + + tracing::debug!(parent: &self.span, + event = "error", + "Failed to write to stream: {:?}", err); + Err(()) + } + + #[inline(always)] + pub async fn read(&mut self, bytes: &mut [u8]) -> Result { + match self.stream.read(bytes).await { + Ok(len) => { + tracing::trace!(parent: &self.span, + event = "read", + data = bytes + .get(0..len) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .unwrap_or("[invalid UTF8]"), + size = len); + Ok(len) + } + Err(err) => { + tracing::debug!( + parent: &self.span, + event = "error", + "Failed to read from stream: {:?}", err + ); + Err(()) + } + } + } +} + +impl Session { + pub async fn get_script_id(&self, account_id: u32, name: &str) -> Result { + self.jmap + .store + .filter( + account_id, + Collection::SieveScript, + vec![Filter::eq(Property::Name, name)], + ) + .await + .map_err(|_| StatusResponse::database_failure()) + .and_then(|results| { + results.results.min().ok_or_else(|| { + StatusResponse::no("There is no script by that name") + .with_code(ResponseCode::NonExistent) + }) + }) + } +} + +trait IsAllowed: Sized { + fn is_allowed(self, imap: &IMAP, state: &State, is_tls: bool) -> Result; +} + +impl IsAllowed for Request { + fn is_allowed(self, imap: &IMAP, state: &State, is_tls: bool) -> Result { + match &self.command { + Command::Capability | Command::Logout | Command::Noop => Ok(self), + Command::Authenticate => { + if let State::NotAuthenticated { .. } = state { + if is_tls { + Ok(self) + } else { + Err(StatusResponse::no("Cannot authenticate over plain-text.") + .with_code(ResponseCode::EncryptNeeded)) + } + } else { + Err(StatusResponse::no("Already authenticated.")) + } + } + Command::StartTls => { + if !is_tls { + Ok(self) + } else { + Err(StatusResponse::no("Already in TLS mode.")) + } + } + Command::HaveSpace + | Command::PutScript + | Command::ListScripts + | Command::SetActive + | Command::GetScript + | Command::DeleteScript + | Command::RenameScript + | Command::CheckScript + | Command::Unauthenticate => { + if let State::Authenticated { access_token, .. } = state { + if imap + .get_authenticated_limiter(access_token.primary_id()) + .lock() + .request_limiter + .is_allowed() + { + Ok(self) + } else { + Err(StatusResponse::no("Too many requests") + .with_code(ResponseCode::TryLater)) + } + } else { + Err(StatusResponse::no("Not authenticated.")) + } + } + } + } +} diff --git a/crates/managesieve/src/core/mod.rs b/crates/managesieve/src/core/mod.rs new file mode 100644 index 00000000..1590ae79 --- /dev/null +++ b/crates/managesieve/src/core/mod.rs @@ -0,0 +1,259 @@ +pub mod client; +pub mod session; + +use std::{borrow::Cow, sync::Arc}; + +use imap::core::IMAP; +use imap_proto::receiver::{CommandParser, Receiver}; +use jmap::{ + auth::{rate_limit::RemoteAddress, AccessToken}, + JMAP, +}; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, +}; +use tokio_rustls::server::TlsStream; +use utils::listener::{limiter::InFlight, ServerInstance}; + +pub struct Session { + pub jmap: Arc, + pub imap: Arc, + pub instance: Arc, + pub receiver: Receiver, + pub state: State, + pub remote_addr: RemoteAddress, + pub stream: T, + pub span: tracing::Span, + pub in_flight: InFlight, +} + +pub enum State { + NotAuthenticated { + auth_failures: u32, + }, + Authenticated { + access_token: Arc, + in_flight: InFlight, + }, +} + +impl State { + pub fn access_token(&self) -> &AccessToken { + match self { + State::Authenticated { access_token, .. } => access_token, + State::NotAuthenticated { .. } => unreachable!("Not authenticated"), + } + } +} + +#[derive(Clone)] +pub struct ManageSieveSessionManager { + pub jmap: Arc, + pub imap: Arc, +} + +impl ManageSieveSessionManager { + pub fn new(jmap: Arc, imap: Arc) -> Self { + Self { jmap, imap } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Command { + Authenticate, + StartTls, + Logout, + Capability, + HaveSpace, + PutScript, + ListScripts, + SetActive, + GetScript, + DeleteScript, + RenameScript, + CheckScript, + Noop, + Unauthenticate, +} + +pub trait IsTls { + fn is_tls(&self) -> bool; +} + +impl IsTls for TcpStream { + fn is_tls(&self) -> bool { + false + } +} + +impl IsTls for TlsStream { + fn is_tls(&self) -> bool { + true + } +} + +impl CommandParser for Command { + fn parse(value: &[u8], _is_uid: bool) -> Option { + match value { + b"AUTHENTICATE" => Some(Command::Authenticate), + b"STARTTLS" => Some(Command::StartTls), + b"LOGOUT" => Some(Command::Logout), + b"CAPABILITY" => Some(Command::Capability), + b"HAVESPACE" => Some(Command::HaveSpace), + b"PUTSCRIPT" => Some(Command::PutScript), + b"LISTSCRIPTS" => Some(Command::ListScripts), + b"SETACTIVE" => Some(Command::SetActive), + b"GETSCRIPT" => Some(Command::GetScript), + b"DELETESCRIPT" => Some(Command::DeleteScript), + b"RENAMESCRIPT" => Some(Command::RenameScript), + b"CHECKSCRIPT" => Some(Command::CheckScript), + b"NOOP" => Some(Command::Noop), + b"UNAUTHENTICATE" => Some(Command::Unauthenticate), + _ => None, + } + } + + fn tokenize_brackets(&self) -> bool { + false + } +} + +impl Default for Command { + fn default() -> Self { + Command::Noop + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StatusResponse { + pub code: Option, + pub message: Cow<'static, str>, + pub rtype: ResponseType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResponseType { + Ok, + No, + Bye, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResponseCode { + AuthTooWeak, + EncryptNeeded, + Quota, + QuotaMaxScripts, + QuotaMaxSize, + Referral, + Sasl, + TransitionNeeded, + TryLater, + Active, + NonExistent, + AlreadyExists, + Tag(String), + Warnings, +} + +impl ResponseCode { + pub fn serialize(&self, buf: &mut Vec) { + buf.extend_from_slice(match self { + ResponseCode::AuthTooWeak => b"AUTH-TOO-WEAK", + ResponseCode::EncryptNeeded => b"ENCRYPT-NEEDED", + ResponseCode::Quota => b"QUOTA", + ResponseCode::QuotaMaxScripts => b"QUOTA/MAXSCRIPTS", + ResponseCode::QuotaMaxSize => b"QUOTA/MAXSIZE", + ResponseCode::Referral => b"REFERRAL", + ResponseCode::Sasl => b"SASL", + ResponseCode::TransitionNeeded => b"TRANSITION-NEEDED", + ResponseCode::TryLater => b"TRYLATER", + ResponseCode::Active => b"ACTIVE", + ResponseCode::NonExistent => b"NONEXISTENT", + ResponseCode::AlreadyExists => b"ALREADYEXISTS", + ResponseCode::Tag(tag) => { + buf.extend_from_slice(b"TAG {"); + buf.extend_from_slice(tag.len().to_string().as_bytes()); + buf.extend_from_slice(b"}\r\n"); + buf.extend_from_slice(tag.as_bytes()); + return; + } + ResponseCode::Warnings => b"WARNINGS", + }); + } +} + +impl ResponseType { + pub fn serialize(&self, buf: &mut Vec) { + buf.extend_from_slice(match self { + ResponseType::Ok => b"OK", + ResponseType::No => b"NO", + ResponseType::Bye => b"BYE", + }); + } +} + +impl StatusResponse { + pub fn serialize(self, mut buf: Vec) -> Vec { + self.rtype.serialize(&mut buf); + if let Some(code) = &self.code { + buf.extend_from_slice(b" ("); + code.serialize(&mut buf); + buf.push(b')'); + } + if !self.message.is_empty() { + buf.extend_from_slice(b" \""); + for ch in self.message.as_bytes() { + if [b'\"', b'\\'].contains(ch) { + buf.push(b'\\'); + } + buf.push(*ch); + } + buf.push(b'\"'); + } + buf.extend_from_slice(b"\r\n"); + buf + } + + pub fn into_bytes(self) -> Vec { + self.serialize(Vec::with_capacity(16)) + } + + pub fn with_code(mut self, code: ResponseCode) -> Self { + self.code = Some(code); + self + } + + pub fn no(message: impl Into>) -> Self { + StatusResponse { + code: None, + message: message.into(), + rtype: ResponseType::No, + } + } + + pub fn ok(message: impl Into>) -> Self { + StatusResponse { + code: None, + message: message.into(), + rtype: ResponseType::Ok, + } + } + + pub fn bye(message: impl Into>) -> Self { + StatusResponse { + code: None, + message: message.into(), + rtype: ResponseType::Bye, + } + } + + pub fn database_failure() -> Self { + StatusResponse { + code: Some(ResponseCode::TryLater), + message: Cow::Borrowed("Database failure"), + rtype: ResponseType::No, + } + } +} diff --git a/crates/managesieve/src/core/session.rs b/crates/managesieve/src/core/session.rs new file mode 100644 index 00000000..164a2e39 --- /dev/null +++ b/crates/managesieve/src/core/session.rs @@ -0,0 +1,144 @@ +use imap_proto::receiver::{self, Receiver}; +use jmap::auth::rate_limit::RemoteAddress; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, +}; +use tokio_rustls::server::TlsStream; +use utils::listener::SessionManager; + +use super::{IsTls, ManageSieveSessionManager, Session, State}; + +impl SessionManager for ManageSieveSessionManager { + fn spawn(&self, session: utils::listener::SessionData) { + // Create session + let session = Session { + jmap: self.jmap.clone(), + imap: self.imap.clone(), + instance: session.instance, + state: State::NotAuthenticated { auth_failures: 0 }, + span: session.span, + stream: session.stream, + in_flight: session.in_flight, + remote_addr: RemoteAddress::IpAddress(session.remote_ip), + receiver: Receiver::with_max_request_size(self.imap.max_request_size) + .with_start_state(receiver::State::Command { is_uid: false }), + }; + + tokio::spawn(async move { + if session.instance.is_tls_implicit { + if let Ok(session) = session.into_tls().await { + session.handle_conn().await; + } + } else { + session.handle_conn().await; + } + }); + } + + fn shutdown(&self) { + // No-op + } +} + +impl Session { + pub async fn handle_conn_(&mut self) -> bool { + let mut buf = vec![0; 8192]; + let mut shutdown_rx = self.instance.shutdown_rx.clone(); + + loop { + tokio::select! { + result = tokio::time::timeout( + if !matches!(self.state, State::NotAuthenticated {..}) { + self.imap.timeout_auth + } else { + self.imap.timeout_unauth + }, + self.read(&mut buf)) => { + match result { + Ok(Ok(bytes_read)) => { + if bytes_read > 0 { + match self.ingest(&buf[..bytes_read]).await { + Ok(true) => (), + Ok(false) => { + return true; + } + Err(_) => { + break; + } + } + } else { + tracing::debug!( + parent: &self.span, + event = "disconnect", + reason = "peer", + "Connection closed by peer." + ); + break; + } + } + Ok(Err(_)) => { + break; + } + Err(_) => { + tracing::debug!( + parent: &self.span, + event = "disconnect", + reason = "timeout", + "Connection timed out." + ); + self + .write(b"BYE \"Connection timed out.\"\r\n") + .await + .ok(); + break; + } + } + }, + _ = shutdown_rx.changed() => { + tracing::debug!( + parent: &self.span, + event = "disconnect", + reason = "shutdown", + "Server shutting down." + ); + self.write(b"BYE \"Server shutting down.\"\r\n").await.ok(); + break; + } + }; + } + + false + } +} + +impl Session { + pub async fn into_tls(self) -> Result>, ()> { + let span = self.span; + Ok(Session { + stream: self.instance.tls_accept(self.stream, &span).await?, + state: self.state, + instance: self.instance, + in_flight: self.in_flight, + span, + jmap: self.jmap, + imap: self.imap, + receiver: self.receiver, + remote_addr: self.remote_addr, + }) + } + + pub async fn handle_conn(mut self) { + if self.handle_conn_().await && self.instance.tls_acceptor.is_some() { + if let Ok(session) = self.into_tls().await { + session.handle_conn().await; + } + } + } +} + +impl Session> { + pub async fn handle_conn(mut self) { + self.handle_conn_().await; + } +} diff --git a/crates/managesieve/src/lib.rs b/crates/managesieve/src/lib.rs new file mode 100644 index 00000000..5f937f62 --- /dev/null +++ b/crates/managesieve/src/lib.rs @@ -0,0 +1,134 @@ +pub mod core; +pub mod op; + +#[cfg(test)] +mod tests { + use imap_proto::receiver::{Error, Receiver, Request, State, Token}; + + use crate::core::Command; + + #[test] + fn receiver_parse_managesieve() { + let mut receiver = Receiver::new().with_start_state(State::Command { is_uid: false }); + + for (frames, expected_requests) in [ + ( + vec!["Authenticate \"DIGEST-MD5\"\r\n"], + vec![Request { + tag: "".to_string(), + command: Command::Authenticate, + tokens: vec![Token::Argument(b"DIGEST-MD5".to_vec())], + }], + ), + ( + vec![ + " AUTHENTICATE \"GSSAPI\" {56+}\r\n", + "cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA==\r\n", + ], + vec![Request { + tag: "".to_string(), + command: Command::Authenticate, + tokens: vec![ + Token::Argument(b"GSSAPI".to_vec()), + Token::Argument( + b"cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA==".to_vec(), + ), + ], + }], + ), + ( + vec!["Authenticate \"PLAIN\" \"QJIrweAPyo6Q1T9xu\"\r\n"], + vec![Request { + tag: "".to_string(), + command: Command::Authenticate, + tokens: vec![ + Token::Argument(b"PLAIN".to_vec()), + Token::Argument(b"QJIrweAPyo6Q1T9xu".to_vec()), + ], + }], + ), + ( + vec!["StartTls\r\n"], + vec![Request { + tag: "".to_string(), + command: Command::StartTls, + tokens: vec![], + }], + ), + ( + vec!["HAVESPACE \"myscript\" 999999\r\n"], + vec![Request { + tag: "".to_string(), + command: Command::HaveSpace, + tokens: vec![ + Token::Argument(b"myscript".to_vec()), + Token::Argument(b"999999".to_vec()), + ], + }], + ), + ( + vec![ + "Putscript \"foo\" {31+}\r\n", + "#comment\r\n", + "InvalidSieveCommand\r\n\r\n", + ], + vec![Request { + tag: "".to_string(), + command: Command::PutScript, + tokens: vec![ + Token::Argument(b"foo".to_vec()), + Token::Argument(b"#comment\r\nInvalidSieveCommand\r\n".to_vec()), + ], + }], + ), + ( + vec!["Listscripts\r\n"], + vec![Request { + tag: "".to_string(), + command: Command::ListScripts, + tokens: vec![], + }], + ), + ( + vec!["Setactive \"baz\"\r\n"], + vec![Request { + tag: "".to_string(), + command: Command::SetActive, + tokens: vec![Token::Argument(b"baz".to_vec())], + }], + ), + ( + vec!["Renamescript \"foo\" \"bar\"\r\n"], + vec![Request { + tag: "".to_string(), + command: Command::RenameScript, + tokens: vec![ + Token::Argument(b"foo".to_vec()), + Token::Argument(b"bar".to_vec()), + ], + }], + ), + ( + vec!["NOOP \"STARTTLS-SYNC-42\"\r\n"], + vec![Request { + tag: "".to_string(), + command: Command::Noop, + tokens: vec![Token::Argument(b"STARTTLS-SYNC-42".to_vec())], + }], + ), + ] { + let mut requests = Vec::new(); + for frame in &frames { + let mut bytes = frame.as_bytes().iter(); + loop { + match receiver.parse(&mut bytes) { + Ok(request) => requests.push(request), + Err(Error::NeedsMoreData | Error::NeedsLiteral { .. }) => break, + Err(err) => panic!("{:?} for frames {:#?}", err, frames), + } + } + } + assert_eq!(requests, expected_requests, "{:#?}", frames); + } + } +} diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs new file mode 100644 index 00000000..6de89943 --- /dev/null +++ b/crates/managesieve/src/op/authenticate.rs @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::sync::Arc; + +use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain}; +use imap_proto::{ + protocol::authenticate::Mechanism, + receiver::{self, Request}, +}; +use mail_parser::decoders::base64::base64_decode; +use mail_send::Credentials; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Command, IsTls, Session, State, StatusResponse}; + +impl Session { + pub async fn handle_authenticate(&mut self, request: Request) -> crate::op::OpResult { + if request.tokens.is_empty() { + return Err(StatusResponse::no("Authentication mechanism missing.")); + } + + let mut tokens = request.tokens.into_iter(); + let mechanism = + Mechanism::parse(&tokens.next().unwrap().unwrap_bytes()).map_err(StatusResponse::no)?; + let mut params: Vec = tokens + .into_iter() + .filter_map(|token| token.unwrap_string().ok()) + .collect(); + + let credentials = match mechanism { + Mechanism::Plain | Mechanism::OAuthBearer => { + if !params.is_empty() { + let challenge = base64_decode(params.pop().unwrap().as_bytes()) + .ok_or_else(|| StatusResponse::no("Failed to decode challenge."))?; + (if mechanism == Mechanism::Plain { + decode_challenge_plain(&challenge) + } else { + decode_challenge_oauth(&challenge) + } + .map_err(StatusResponse::no))? + } else { + self.receiver.request = receiver::Request { + tag: String::new(), + command: Command::Authenticate, + tokens: vec![receiver::Token::Argument(mechanism.into_bytes())], + }; + self.receiver.state = receiver::State::Argument { last_ch: b' ' }; + return Ok(b"{0}\r\n".to_vec()); + } + } + _ => { + return Err(StatusResponse::no( + "Authentication mechanism not supported.", + )) + } + }; + + // Throttle authentication requests + if self.jmap.is_auth_allowed(self.remote_addr.clone()).is_err() { + tracing::debug!(parent: &self.span, + event = "disconnect", + "Too many authentication attempts, disconnecting.", + ); + return Err(StatusResponse::bye( + "Too many authentication requests from this IP address.", + )); + } + + // Authenticate + let access_token = match credentials { + Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => { + self.jmap.authenticate_plain(&username, &secret).await + } + Credentials::OAuthBearer { token } => { + match self + .jmap + .validate_access_token("access_token", &token) + .await + { + Ok((account_id, _, _)) => self.jmap.get_access_token(account_id).await, + Err(err) => { + tracing::debug!( + parent: &self.span, + context = "authenticate", + err = err, + "Failed to validate access token." + ); + None + } + } + } + }; + + if let Some(access_token) = access_token { + // Enforce concurrency limits + let in_flight = self + .imap + .get_authenticated_limiter(access_token.primary_id()) + .lock() + .concurrent_requests + .is_allowed(); + if let Some(in_flight) = in_flight { + // Cache access token + let access_token = Arc::new(access_token); + self.jmap.cache_access_token(access_token.clone()); + + // Create session + self.state = State::Authenticated { + access_token, + in_flight, + }; + + self.handle_capability("Authentication successful").await + } else { + tracing::debug!(parent: &self.span, + event = "disconnect", + "Too many concurrent connection.", + ); + Err(StatusResponse::bye("Too many concurrent connections.")) + } + } else { + match &self.state { + State::NotAuthenticated { auth_failures } + if *auth_failures < self.imap.max_auth_failures => + { + self.state = State::NotAuthenticated { + auth_failures: auth_failures + 1, + }; + Ok(StatusResponse::no("Authentication failed").into_bytes()) + } + _ => { + tracing::debug!( + parent: &self.span, + event = "disconnect", + "Too many authentication failures, disconnecting.", + ); + Err(StatusResponse::bye("Too many authentication failures")) + } + } + } + } + + pub async fn handle_unauthenticate(&mut self) -> super::OpResult { + self.state = State::NotAuthenticated { auth_failures: 0 }; + + Ok(StatusResponse::ok("Unauthenticate successful.").into_bytes()) + } +} diff --git a/crates/managesieve/src/op/capability.rs b/crates/managesieve/src/op/capability.rs new file mode 100644 index 00000000..d02b1f1b --- /dev/null +++ b/crates/managesieve/src/op/capability.rs @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use jmap::api::session::Capabilities; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{IsTls, Session, StatusResponse}; + +impl Session { + pub async fn handle_capability(&mut self, message: &'static str) -> super::OpResult { + let mut response = Vec::with_capacity(128); + response.extend_from_slice(b"\"IMPLEMENTATION\" \"Stalwart ManageSieve v"); + response.extend_from_slice(env!("CARGO_PKG_VERSION").as_bytes()); + response.extend_from_slice(b"\"\r\n"); + response.extend_from_slice(b"\"VERSION\" \"1.0\"\r\n"); + if !self.stream.is_tls() { + response.extend_from_slice(b"\"SASL\" \"\"\r\n"); + response.extend_from_slice(b"\"STARTTLS\"\r\n"); + } else { + response.extend_from_slice(b"\"SASL\" \"PLAIN OAUTHBEARER\"\r\n"); + }; + if let Some(sieve) = + self.jmap + .config + .capabilities + .capabilities + .iter() + .find_map(|(_, item)| { + if let Capabilities::Sieve(sieve) = item { + Some(sieve) + } else { + None + } + }) + { + response.extend_from_slice(b"\"SIEVE\" \""); + response.extend_from_slice(sieve.extensions.join(" ").as_bytes()); + response.extend_from_slice(b"\"\r\n"); + if let Some(notification_methods) = &sieve.notification_methods { + response.extend_from_slice(b"\"NOTIFY\" \""); + response.extend_from_slice(notification_methods.join(" ").as_bytes()); + response.extend_from_slice(b"\"\r\n"); + } + if sieve.max_redirects > 0 { + response.extend_from_slice(b"\"MAXREDIRECTS\" \""); + response.extend_from_slice(sieve.max_redirects.to_string().as_bytes()); + response.extend_from_slice(b"\"\r\n"); + } + } else { + response.extend_from_slice(b"\"SIEVE\" \"\"\r\n"); + } + + Ok(StatusResponse::ok(message).serialize(response)) + } +} diff --git a/crates/managesieve/src/op/checkscript.rs b/crates/managesieve/src/op/checkscript.rs new file mode 100644 index 00000000..c8a75339 --- /dev/null +++ b/crates/managesieve/src/op/checkscript.rs @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::receiver::Request; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Command, Session, StatusResponse}; + +impl Session { + pub async fn handle_checkscript(&mut self, request: Request) -> super::OpResult { + if request.tokens.is_empty() { + return Err(StatusResponse::no("Expected script as a parameter.")); + } + + self.jmap + .sieve_compiler + .compile(&request.tokens.into_iter().next().unwrap().unwrap_bytes()) + .map(|_| StatusResponse::ok("Script is valid.").into_bytes()) + .map_err(|err| StatusResponse::no(err.to_string())) + } +} diff --git a/crates/managesieve/src/op/deletescript.rs b/crates/managesieve/src/op/deletescript.rs new file mode 100644 index 00000000..7ebae30f --- /dev/null +++ b/crates/managesieve/src/op/deletescript.rs @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::receiver::Request; +use jmap_proto::types::collection::Collection; +use store::write::log::ChangeLogBuilder; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Command, ResponseCode, Session, StatusResponse}; + +impl Session { + pub async fn handle_deletescript(&mut self, request: Request) -> super::OpResult { + let name = request + .tokens + .into_iter() + .next() + .and_then(|s| s.unwrap_string().ok()) + .ok_or_else(|| StatusResponse::no("Expected script name as a parameter."))?; + + let account_id = self.state.access_token().primary_id(); + let document_id = self.get_script_id(account_id, &name).await?; + if self + .jmap + .sieve_script_delete(account_id, document_id, true) + .await? + { + // Write changes + let mut changelog = ChangeLogBuilder::new(); + changelog.log_delete(Collection::SieveScript, document_id); + self.jmap.commit_changes(account_id, changelog).await?; + + Ok(StatusResponse::ok("Deleted.").into_bytes()) + } else { + Err(StatusResponse::no("You may not delete an active script") + .with_code(ResponseCode::Active)) + } + } +} diff --git a/crates/managesieve/src/op/getscript.rs b/crates/managesieve/src/op/getscript.rs new file mode 100644 index 00000000..3af05ca6 --- /dev/null +++ b/crates/managesieve/src/op/getscript.rs @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::receiver::Request; +use jmap_proto::{ + object::Object, + types::{collection::Collection, property::Property, value::Value}, +}; +use store::BlobKind; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Command, ResponseCode, Session, StatusResponse}; + +impl Session { + pub async fn handle_getscript(&mut self, request: Request) -> super::OpResult { + let name = request + .tokens + .into_iter() + .next() + .and_then(|s| s.unwrap_string().ok()) + .ok_or_else(|| StatusResponse::no("Expected script name as a parameter."))?; + let account_id = self.state.access_token().primary_id(); + let document_id = self.get_script_id(account_id, &name).await?; + let script_size = self + .jmap + .get_property::>( + account_id, + Collection::SieveScript, + document_id, + Property::Value, + ) + .await? + .ok_or_else(|| { + StatusResponse::no("Script not found").with_code(ResponseCode::NonExistent) + })? + .remove(&Property::BlobId) + .try_unwrap_uint() + .ok_or_else(|| { + StatusResponse::no("Filed to retrieve blob size").with_code(ResponseCode::TryLater) + })? as u32; + let script = self + .jmap + .get_blob( + &BlobKind::Linked { + account_id, + collection: Collection::SieveScript.into(), + document_id, + }, + 0..script_size, + ) + .await? + .ok_or_else(|| { + StatusResponse::no("Script blob not found").with_code(ResponseCode::NonExistent) + })?; + + let mut response = Vec::with_capacity(script.len() + 30); + response.push(b'{'); + response.extend_from_slice(script_size.to_string().as_bytes()); + response.extend_from_slice(b"}\r\n"); + response.extend(script); + + Ok(StatusResponse::ok("").serialize(response)) + } +} diff --git a/crates/managesieve/src/op/havespace.rs b/crates/managesieve/src/op/havespace.rs new file mode 100644 index 00000000..09904833 --- /dev/null +++ b/crates/managesieve/src/op/havespace.rs @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::receiver::Request; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Command, ResponseCode, Session, StatusResponse}; + +impl Session { + pub async fn handle_havespace(&mut self, request: Request) -> crate::op::OpResult { + let mut tokens = request.tokens.into_iter(); + let name = tokens + .next() + .and_then(|s| s.unwrap_string().ok()) + .ok_or_else(|| StatusResponse::no("Expected script name as a parameter."))?; + let size: usize = tokens + .next() + .and_then(|s| s.unwrap_string().ok()) + .ok_or_else(|| StatusResponse::no("Expected script size as a parameter."))? + .parse::() + .map_err(|_| StatusResponse::no("Invalid size parameter."))?; + + // Validate name + let access_token = self.state.access_token(); + let account_id = access_token.primary_id(); + self.validate_name(account_id, &name).await?; + + // Validate quota + if access_token.quota == 0 + || size as i64 + self.jmap.get_used_quota(account_id).await? + <= access_token.quota as i64 + { + Ok(StatusResponse::ok("").into_bytes()) + } else { + Err(StatusResponse::no("Quota exceeded.").with_code(ResponseCode::QuotaMaxSize)) + } + } +} diff --git a/crates/managesieve/src/op/listscripts.rs b/crates/managesieve/src/op/listscripts.rs new file mode 100644 index 00000000..c044274e --- /dev/null +++ b/crates/managesieve/src/op/listscripts.rs @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use jmap_proto::{ + object::Object, + types::{collection::Collection, property::Property, value::Value}, +}; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Session, StatusResponse}; + +impl Session { + pub async fn handle_listscripts(&mut self) -> super::OpResult { + let account_id = self.state.access_token().primary_id(); + let document_ids = self + .jmap + .get_document_ids(account_id, Collection::SieveScript) + .await? + .unwrap_or_default(); + + if document_ids.is_empty() { + return Ok(StatusResponse::ok("").into_bytes()); + } + + let mut response = Vec::with_capacity(128); + + for document_id in document_ids { + if let Some(script) = self + .jmap + .get_property::>( + account_id, + Collection::SieveScript, + document_id, + Property::Value, + ) + .await? + { + response.push(b'\"'); + if let Some(name) = script.get(&Property::Name).as_string() { + for ch in name.as_bytes() { + if [b'\\', b'\"'].contains(ch) { + response.push(b'\\'); + } + response.push(*ch); + } + } + + if script.get(&Property::IsActive).as_bool() == Some(true) { + response.extend_from_slice(b"\" ACTIVE\r\n"); + } else { + response.extend_from_slice(b"\"\r\n"); + } + } + } + + Ok(StatusResponse::ok("").serialize(response)) + } +} diff --git a/crates/managesieve/src/op/logout.rs b/crates/managesieve/src/op/logout.rs new file mode 100644 index 00000000..e98147d7 --- /dev/null +++ b/crates/managesieve/src/op/logout.rs @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Session, StatusResponse}; + +impl Session { + pub async fn handle_logout(&mut self) -> super::OpResult { + Err(StatusResponse::bye(concat!( + "Stalwart ManageSieve v", + env!("CARGO_PKG_VERSION"), + " bids you farewell." + ))) + } +} diff --git a/crates/managesieve/src/op/mod.rs b/crates/managesieve/src/op/mod.rs new file mode 100644 index 00000000..47642bbf --- /dev/null +++ b/crates/managesieve/src/op/mod.rs @@ -0,0 +1,24 @@ +use jmap_proto::error::method::MethodError; + +use crate::core::StatusResponse; + +pub mod authenticate; +pub mod capability; +pub mod checkscript; +pub mod deletescript; +pub mod getscript; +pub mod havespace; +pub mod listscripts; +pub mod logout; +pub mod noop; +pub mod putscript; +pub mod renamescript; +pub mod setactive; + +impl From for StatusResponse { + fn from(_: MethodError) -> Self { + StatusResponse::database_failure() + } +} + +pub type OpResult = std::result::Result, StatusResponse>; diff --git a/crates/managesieve/src/op/noop.rs b/crates/managesieve/src/op/noop.rs new file mode 100644 index 00000000..58679592 --- /dev/null +++ b/crates/managesieve/src/op/noop.rs @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::receiver::Request; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Command, ResponseCode, Session, StatusResponse}; + +impl Session { + pub async fn handle_noop(&mut self, request: Request) -> super::OpResult { + Ok(if let Some(tag) = request + .tokens + .into_iter() + .next() + .and_then(|t| t.unwrap_string().ok()) + { + StatusResponse::ok("Done").with_code(ResponseCode::Tag(tag)) + } else { + StatusResponse::ok("Done") + } + .into_bytes()) + } +} diff --git a/crates/managesieve/src/op/putscript.rs b/crates/managesieve/src/op/putscript.rs new file mode 100644 index 00000000..dabde984 --- /dev/null +++ b/crates/managesieve/src/op/putscript.rs @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::receiver::Request; +use jmap::sieve::set::SCHEMA; +use jmap_proto::{ + object::{index::ObjectIndexBuilder, Object}, + types::{blob::BlobId, collection::Collection, property::Property, value::Value}, +}; +use sieve::compiler::ErrorType; +use store::{query::Filter, write::BatchBuilder}; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Command, ResponseCode, Session, StatusResponse}; + +impl Session { + pub async fn handle_putscript(&mut self, request: Request) -> super::OpResult { + let mut tokens = request.tokens.into_iter(); + let name = tokens + .next() + .and_then(|s| s.unwrap_string().ok()) + .ok_or_else(|| StatusResponse::no("Expected script name as a parameter."))? + .trim() + .to_string(); + let mut script = tokens + .next() + .ok_or_else(|| StatusResponse::no("Expected script as a parameter."))? + .unwrap_bytes(); + + // Check quota + let access_token = self.state.access_token(); + let account_id = access_token.primary_id(); + if access_token.quota > 0 + && script.len() as i64 + self.jmap.get_used_quota(account_id).await? + > access_token.quota as i64 + { + return Err(StatusResponse::no("Quota exceeded.").with_code(ResponseCode::Quota)); + } + if self + .jmap + .get_document_ids(account_id, Collection::SieveScript) + .await? + .map(|ids| ids.len() as usize) + .unwrap_or(0) + > self.jmap.config.sieve_max_scripts + { + return Err( + StatusResponse::no("Too many scripts.").with_code(ResponseCode::QuotaMaxScripts) + ); + } + + // Validate name + self.validate_name(account_id, &name).await?; + + // Compile script + match self.jmap.sieve_compiler.compile(&script) { + Ok(compiled_script) => { + script.extend(bincode::serialize(&compiled_script).unwrap_or_default()); + } + Err(err) => { + return Err(if let ErrorType::ScriptTooLong = &err.error_type() { + StatusResponse::no(err.to_string()).with_code(ResponseCode::QuotaMaxSize) + } else { + StatusResponse::no(err.to_string()) + }); + } + } + + // Obtain document id + let document_id = self + .jmap + .assign_document_id(account_id, Collection::SieveScript) + .await?; + + // Store blob + self.jmap + .put_blob( + &BlobId::linked(account_id, Collection::SieveScript, document_id).kind, + &script, + ) + .await?; + + // Write record + let mut changelog = self.jmap.begin_changes(account_id).await?; + changelog.log_insert(Collection::SieveScript, document_id); + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::SieveScript) + .create_document(document_id) + .custom( + ObjectIndexBuilder::new(SCHEMA).with_changes( + Object::with_capacity(3) + .with_property(Property::Name, name) + .with_property(Property::IsActive, Value::Bool(false)) + .with_property(Property::Size, Value::UnsignedInt(script.len() as u64)), + ), + ) + .custom(changelog); + self.jmap.write_batch(batch).await?; + + Ok(StatusResponse::ok("Success.").into_bytes()) + } + + pub async fn validate_name(&self, account_id: u32, name: &str) -> Result<(), StatusResponse> { + if name.is_empty() { + Err(StatusResponse::no("Script name cannot be empty.")) + } else if name.len() > self.jmap.config.sieve_max_script_name { + Err(StatusResponse::no("Script name is too long.")) + } else if name.eq_ignore_ascii_case("vacation") { + Err(StatusResponse::no( + "The 'vacation' name is reserved, please use a different name.", + )) + } else if !self + .jmap + .filter( + account_id, + Collection::SieveScript, + vec![Filter::eq(Property::Name, name)], + ) + .await? + .results + .is_empty() + { + Err( + StatusResponse::no(format!("A sieve script with name '{name}' already exists.",)) + .with_code(ResponseCode::AlreadyExists), + ) + } else { + Ok(()) + } + } +} diff --git a/crates/managesieve/src/op/renamescript.rs b/crates/managesieve/src/op/renamescript.rs new file mode 100644 index 00000000..4d171f71 --- /dev/null +++ b/crates/managesieve/src/op/renamescript.rs @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::receiver::Request; +use jmap::sieve::set::SCHEMA; +use jmap_proto::{ + error::method::MethodError, + object::{index::ObjectIndexBuilder, Object}, + types::{collection::Collection, property::Property, value::Value}, +}; +use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder}; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Command, ResponseCode, Session, StatusResponse}; + +impl Session { + pub async fn handle_renamescript(&mut self, request: Request) -> super::OpResult { + let mut tokens = request.tokens.into_iter(); + let name = tokens + .next() + .and_then(|s| s.unwrap_string().ok()) + .ok_or_else(|| StatusResponse::no("Expected old script name as a parameter."))? + .trim() + .to_string(); + let new_name = tokens + .next() + .and_then(|s| s.unwrap_string().ok()) + .ok_or_else(|| StatusResponse::no("Expected new script name as a parameter."))? + .trim() + .to_string(); + + // Validate name + if name == new_name { + return Ok(StatusResponse::ok("Old and new script names are the same.").into_bytes()); + } + let account_id = self.state.access_token().primary_id(); + let document_id = self.get_script_id(account_id, &name).await?; + self.validate_name(account_id, &new_name).await?; + + // Obtain script values + let script = self + .jmap + .get_property::>>( + account_id, + Collection::SieveScript, + document_id, + Property::Value, + ) + .await? + .ok_or_else(|| { + StatusResponse::no("Script not found").with_code(ResponseCode::NonExistent) + })?; + + // Write record + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::SieveScript) + .update_document(document_id) + .custom( + ObjectIndexBuilder::new(SCHEMA) + .with_current(script) + .with_changes(Object::with_capacity(1).with_property(Property::Name, new_name)), + ); + if !batch.is_empty() { + match self.jmap.write_batch(batch).await { + Ok(_) => { + let mut changelog = ChangeLogBuilder::new(); + changelog.log_update(Collection::SieveScript, document_id); + self.jmap.commit_changes(account_id, changelog).await?; + } + Err(MethodError::ServerUnavailable) => { + return Err(StatusResponse::no( + "Another process modified this script, please try again.", + ) + .with_code(ResponseCode::TryLater)); + } + Err(_) => { + return Err(StatusResponse::database_failure()); + } + } + } + + Ok(StatusResponse::ok("Success.").into_bytes()) + } +} diff --git a/crates/managesieve/src/op/setactive.rs b/crates/managesieve/src/op/setactive.rs new file mode 100644 index 00000000..fafea0de --- /dev/null +++ b/crates/managesieve/src/op/setactive.rs @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::receiver::Request; +use jmap_proto::types::collection::Collection; +use store::write::log::ChangeLogBuilder; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::core::{Command, Session, StatusResponse}; + +impl Session { + pub async fn handle_setactive(&mut self, request: Request) -> super::OpResult { + let name = request + .tokens + .into_iter() + .next() + .and_then(|s| s.unwrap_string().ok()) + .ok_or_else(|| StatusResponse::no("Expected script name as a parameter."))?; + + // De/activate script + let account_id = self.state.access_token().primary_id(); + let changes = self + .jmap + .sieve_activate_script( + account_id, + if !name.is_empty() { + self.get_script_id(account_id, &name).await?.into() + } else { + None + }, + ) + .await?; + + // Write changes + if !changes.is_empty() { + let mut changelog = ChangeLogBuilder::new(); + for (document_id, _) in changes { + changelog.log_update(Collection::SieveScript, document_id); + } + self.jmap.commit_changes(account_id, changelog).await?; + } + Ok(StatusResponse::ok("Success").into_bytes()) + } +} diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 320d5e3f..5ef2f58f 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -20,6 +20,8 @@ tracing-opentelemetry = "0.18.0" opentelemetry = { version = "0.18.0", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.11.0", features = ["http-proto", "reqwest-client"] } opentelemetry-semantic-conventions = { version = "0.10.0" } +dashmap = "5.4" +ahash = { version = "0.8" } [target.'cfg(unix)'.dependencies] privdrop = "0.5.3" diff --git a/crates/utils/src/config/listener.rs b/crates/utils/src/config/listener.rs index 1863065b..d41e5235 100644 --- a/crates/utils/src/config/listener.rs +++ b/crates/utils/src/config/listener.rs @@ -287,7 +287,7 @@ impl Config { .value_or_default(("server.listener", id, "url"), "server.url") .failed(&format!("No 'url' directive found for listener {id:?}")) .to_string(), - ServerProtocol::Imap | ServerProtocol::Http => self + ServerProtocol::Imap | ServerProtocol::Http | ServerProtocol::ManageSieve => self .value_or_default(("server.listener", id, "url"), "server.url") .unwrap_or_default() .to_string(), @@ -318,6 +318,8 @@ impl ParseValue for ServerProtocol { Ok(Self::Imap) } else if value.eq_ignore_ascii_case("http") { Ok(Self::Http) + } else if value.eq_ignore_ascii_case("managesieve") { + Ok(Self::ManageSieve) } else { Err(format!( "Invalid server protocol type {:?} for property {:?}.", diff --git a/crates/utils/src/config/mod.rs b/crates/utils/src/config/mod.rs index accc09b8..1057bb2f 100644 --- a/crates/utils/src/config/mod.rs +++ b/crates/utils/src/config/mod.rs @@ -71,6 +71,7 @@ pub enum ServerProtocol { Jmap, Imap, Http, + ManageSieve, } #[derive(Debug, Default, PartialEq, Eq, Clone)] @@ -87,6 +88,7 @@ impl Display for ServerProtocol { ServerProtocol::Jmap => write!(f, "jmap"), ServerProtocol::Imap => write!(f, "imap"), ServerProtocol::Http => write!(f, "http"), + ServerProtocol::ManageSieve => write!(f, "managesieve"), } } } diff --git a/crates/utils/src/listener/limiter.rs b/crates/utils/src/listener/limiter.rs index 98744235..aa5f447f 100644 --- a/crates/utils/src/listener/limiter.rs +++ b/crates/utils/src/listener/limiter.rs @@ -95,6 +95,10 @@ impl RateLimiter { self.last_refill = Instant::now(); self.tokens = self.max_requests; } + + pub fn is_active(&self) -> bool { + self.tokens < self.max_requests || self.last_refill.elapsed() < self.max_interval + } } impl ConcurrencyLimiter { @@ -120,4 +124,8 @@ impl ConcurrencyLimiter { pub fn check_is_allowed(&self) -> bool { self.concurrent.load(Ordering::Relaxed) < self.max_concurrent } + + pub fn is_active(&self) -> bool { + self.concurrent.load(Ordering::Relaxed) > 0 + } } diff --git a/crates/utils/src/map/mod.rs b/crates/utils/src/map/mod.rs index 6cac8af9..970c2bee 100644 --- a/crates/utils/src/map/mod.rs +++ b/crates/utils/src/map/mod.rs @@ -22,4 +22,5 @@ */ pub mod bitmap; +pub mod ttl_dashmap; pub mod vec_map; diff --git a/crates/utils/src/map/ttl_dashmap.rs b/crates/utils/src/map/ttl_dashmap.rs new file mode 100644 index 00000000..c74ee6cf --- /dev/null +++ b/crates/utils/src/map/ttl_dashmap.rs @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020-2023, Stalwart Labs Ltd. + * + * Licensed under the Apache License, Version 2.0 or the MIT license + * , at your + * option. This file may not be copied, modified, or distributed + * except according to those terms. + */ + +use std::{borrow::Borrow, hash::Hash, time::Instant}; + +use dashmap::DashMap; + +pub type TtlDashMap = DashMap, ahash::RandomState>; + +#[derive(Debug, Clone)] +pub struct LruItem { + item: V, + valid_until: Instant, +} + +pub trait TtlMap: Sized { + fn with_capacity(capacity: usize, shard_amount: usize) -> Self; + fn get_with_ttl(&self, name: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq; + fn insert_with_ttl(&self, name: K, value: V, valid_until: Instant) -> V; + fn cleanup(&self); +} + +impl TtlMap for TtlDashMap { + fn with_capacity(capacity: usize, shard_amount: usize) -> Self { + DashMap::with_capacity_and_hasher_and_shard_amount( + capacity, + ahash::RandomState::new(), + shard_amount, + ) + } + + fn get_with_ttl(&self, name: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq, + { + match self.get(name) { + Some(entry) if entry.valid_until >= Instant::now() => entry.item.clone().into(), + _ => None, + } + } + + fn insert_with_ttl(&self, name: K, item: V, valid_until: Instant) -> V { + self.insert( + name, + LruItem { + item: item.clone(), + valid_until, + }, + ); + item + } + + fn cleanup(&self) { + self.retain(|_, entry| entry.valid_until >= Instant::now()); + } +} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index b389f44d..d07db74a 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -17,6 +17,7 @@ jmap_proto = { path = "../crates/jmap-proto" } imap = { path = "../crates/imap", features = ["test_mode"] } imap_proto = { path = "../crates/imap-proto" } smtp = { path = "../crates/smtp", features = ["test_mode", "local_delivery"] } +managesieve = { path = "../crates/managesieve", features = ["test_mode"] } smtp-proto = { git = "https://github.com/stalwartlabs/smtp-proto" } mail-send = { git = "https://github.com/stalwartlabs/mail-send" } mail-auth = { git = "https://github.com/stalwartlabs/mail-auth", features = ["test"] } diff --git a/tests/resources/imap-test/append b/tests/resources/imap-test/append new file mode 100644 index 00000000..19d5f964 --- /dev/null +++ b/tests/resources/imap-test/append @@ -0,0 +1,43 @@ +connections: 3 +state: created + +1 ok select $mailbox +2 ok select $mailbox + +# Two connections have mailbox SELECTed, one doesn't. +# The \recent flags can be given to either one of the SELECTed connections, +# but never for the 3rd. We rely on mailbox state tracking to catch duplicate +# \recent flags (which is why there are two FETCH FLAGS commands). + +1 ok append $mailbox (\seen \flagged) +* 1 exists +2 ok noop +* 1 exists +3 ok status $mailbox (messages unseen recent) +* status $mailbox (messages 1 unseen 0 recent 0) +1 ok fetch 1 (uid flags) +* 1 fetch (uid $uid1 flags (\seen \flagged)) +2 ok fetch 1 (uid flags) +* 1 fetch (uid $uid1 flags (\seen \flagged)) + +2 ok append +* 2 exists +1 ok noop +* 2 exists +3 ok status $mailbox (messages unseen recent) +* status $mailbox (messages 2 unseen 1 recent 0) +1 ok fetch 2 (uid flags) +2 ok fetch 2 (uid flags) + +3 ok append +3 ok status $mailbox (messages unseen) +* status $mailbox (messages 3 unseen 2) +2 ok noop +* 3 exists +1 ok noop +* 3 exists +1 ok fetch 3 (uid flags) +2 ok fetch 3 (uid flags) + +1 append ${mailbox}nonexistent +no [trycreate] diff --git a/tests/resources/imap-test/append-binary b/tests/resources/imap-test/append-binary new file mode 100644 index 00000000..8977081c Binary files /dev/null and b/tests/resources/imap-test/append-binary differ diff --git a/tests/resources/imap-test/atoms b/tests/resources/imap-test/atoms new file mode 100644 index 00000000..1faca7c5 --- /dev/null +++ b/tests/resources/imap-test/atoms @@ -0,0 +1,13 @@ +state: auth + +# Don't confuse with ~{literal8} +ok list "" ~foo +no select ~foo + +# atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / +# quoted-specials / resp-specials +# quoted-specials = DQUOTE / "\" +# resp-specials = "]" +# list-wildcards = "%" / "*" +no select !#$$&'+,-.0123456789:;<=>?@^_`|[} +ok list "" !#$$&'+,-.0123456789:;<=>?@^_`|[}%* diff --git a/tests/resources/imap-test/broken/search-intdate b/tests/resources/imap-test/broken/search-intdate new file mode 100644 index 00000000..77412fd3 --- /dev/null +++ b/tests/resources/imap-test/broken/search-intdate @@ -0,0 +1,75 @@ +# this test assumes that server stores INTERNALDATE timestamps in +# EET/EEST timezones (or stores/fetches them using the same timezones as +# they were APPENDed with) + +# 1) Timezone changes from EET +0200 -> EEST +0300 +# 1a) BEFORE +ok search before 24-mar-2007 +* search +ok search before 25-mar-2007 +* search 1 +ok search before 26-mar-2007 +* search 1 2 3 4 5 6 +ok search before 27-mar-2007 +* search 1 2 3 4 5 6 7 + +# 1b) ON +ok search on 23-mar-2007 +* search +ok search on 24-mar-2007 +* search 1 +ok search on 25-mar-2007 +* search 2 3 4 5 6 +ok search on 26-mar-2007 +* search 7 + +# 1c) SINCE +ok search 1:7 since 24-mar-2007 +* search 1 2 3 4 5 6 7 +ok search 1:7 since 25-mar-2007 +* search 2 3 4 5 6 7 +ok search 1:7 since 26-mar-2007 +* search 7 +ok search 1:7 since 27-mar-2007 +* search + +# 2) Timezone changes from EEST +0300 -> EET +0200 +# 2a) BEFORE +ok search 8:* before 27-oct-2007 +* search +ok search 8:* before 28-oct-2007 +* search 8 +ok search 8:* before 29-oct-2007 +* search 8 9 10 11 12 13 14 15 +ok search 8:* before 30-oct-2007 +* search 8 9 10 11 12 13 14 15 16 + +# 2b) ON +ok search 8:* on 26-oct-2007 +* search +ok search 8:* on 27-oct-2007 +* search 8 +ok search 8:* on 28-oct-2007 +* search 9 10 11 12 13 14 15 +ok search 8:* on 29-oct-2007 +* search 16 + +# 2c) SINCE +ok search 8:* since 27-oct-2007 +* search 8 9 10 11 12 13 14 15 16 +ok search 8:* since 28-oct-2007 +* search 9 10 11 12 13 14 15 16 +ok search 8:* since 29-oct-2007 +* search 16 +ok search 8:* since 30-oct-2007 +* search + +# 3) Try a couple of NOTs +ok search 1:7 not before 26-mar-2007 +* search 7 +ok search 1:7 not on 25-mar-2007 +* search 1 7 +ok search 8:* not since 28-oct-2007 +* search 8 +ok search 8:* not on 28-oct-2007 +* search 8 16 diff --git a/tests/resources/imap-test/broken/search-intdate.mbox b/tests/resources/imap-test/broken/search-intdate.mbox new file mode 100644 index 00000000..237db794 --- /dev/null +++ b/tests/resources/imap-test/broken/search-intdate.mbox @@ -0,0 +1,80 @@ +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Mar 25 00:00:00 2007 +0200 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Mar 25 01:00:00 2007 +0200 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Mar 25 02:00:00 2007 +0200 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Mar 25 04:00:00 2007 +0300 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Mar 25 23:00:00 2007 +0300 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Mon Mar 26 00:00:00 2007 +0300 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sat Oct 27 23:00:00 2007 +0300 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Oct 28 00:00:00 2007 +0300 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Oct 28 01:00:00 2007 +0300 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Oct 28 02:00:00 2007 +0300 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Oct 28 03:00:00 2007 +0300 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Oct 28 03:00:00 2007 +0200 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Oct 28 04:00:00 2007 +0200 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sun Oct 28 23:00:00 2007 +0200 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Mon Oct 29 00:00:00 2007 +0200 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + diff --git a/tests/resources/imap-test/catenate b/tests/resources/imap-test/catenate new file mode 100644 index 00000000..c56be177 --- /dev/null +++ b/tests/resources/imap-test/catenate @@ -0,0 +1,235 @@ +state: created +capabilities: CATENATE + +"" delete ${mailbox}2 + +ok append $mailbox (\seen \flagged) catenate (text {{{ +From: foo@example.com + +Hello world body + +}}}) + +ok append $mailbox (\seen \flagged) catenate (text {{{ +From: foo2@example.com + +Lookslike: header + +Another body + +}}}) + +ok select $mailbox +* ok [uidvalidity $uidv] + +ok fetch 1:2 (uid body.peek[]) +* 1 fetch (uid $uid body[] {{{ +From: foo@example.com + +Hello world body + +}}}) +* 2 fetch (uid $uid2 body[] {{{ +From: foo2@example.com + +Lookslike: header + +Another body + +}}}) + +ok create ${mailbox}2 + +ok append ${mailbox}2 (\seen \flagged) catenate (url "/$mailbox_url/;uid=$uid/;section=header" text {{{ +body1 + +}}}) + +ok append ${mailbox}2 (\seen \flagged) catenate (url "/$mailbox_url;uidvalidity=$uidv/;uid=$uid/;section=header" text {{{ +body2 + +}}}) + +ok append ${mailbox}2 (\seen \flagged) catenate (url "/$mailbox_url/;uid=$uid2/;section=text" text {{{ +body3 + +}}}) + +ok append ${mailbox}2 (\seen \flagged) catenate (url "/$mailbox_url/;uid=$uid2/;section=1" text {{{ +body4 + +}}}) + +ok append ${mailbox}2 (\seen \flagged) catenate (text {{{ +From: new@example.com +Subject: test header + + +}}} url "/$mailbox_url/;uid=$uid/;section=1" text {{{ + +suffix + +}}}) + +ok append ${mailbox}2 (\seen \flagged) catenate (text {{{ +From: hdr1@example.com + +}}} text {{{ +Subject: hdr2 + +}}} text {{{ + +body6 + +}}}) + +ok append ${mailbox}2 (\seen \flagged) catenate (url "/$mailbox_url/;uid=$uid2/;partial=100000.1" text {{{ +Hdr: foo + +body7 + +}}}) + +# +# Try invalid URLs +# + +no append ${mailbox}2 (\seen \flagged) catenate (url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header" text {{{ +body + +}}}) + +no append ${mailbox}2 (\seen \flagged) catenate (text {{{ +hdr: 1 + +}}} url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header") + +no append ${mailbox}2 (\seen \flagged) catenate (text {{{ +hdr: 1 + +}}} url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header" text {{{ +hdr: 2 + +}}}) + +no append ${mailbox}2 (\seen \flagged) catenate (text {{{ +hdr: 1 + +}}} url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header" text {{{ +hdr: 2 + +}}} url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header" text {{{ +hdr: 3 + +}}}) + +no append ${mailbox}2 (\seen \flagged) catenate (text {{{ +hdr: 1 + +}}} url "/$mailbox_url/;uid=$uid/;section=header" text {{{ +hdr: 2 + +}}} url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header" text {{{ +hdr: 3 + +}}}) + +no append ${mailbox}2 (\seen \flagged) catenate (text {{{ +hdr: 1 + +}}} url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header" text {{{ +hdr: 2 + +}}} url "/$mailbox_url/;uid=$uid/;section=header" text {{{ +hdr: 3 + +}}}) + +no append ${mailbox}2 (\seen \flagged) catenate (url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header" url "/$mailbox_url/;uid=$uid/;section=header") +no append ${mailbox}2 (\seen \flagged) catenate (url "/$mailbox_url/;uid=$uid/;section=header" url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header") + +# +# verify previous appends +# +ok select ${mailbox}2 +ok fetch 1:* body.peek[] +* 1 fetch (body[] {{{ +From: foo@example.com + +body1 + +}}}) +* 2 fetch (body[] {{{ +From: foo@example.com + +body2 + +}}}) +* 3 fetch (body[] {{{ +Lookslike: header + +Another body +body3 + +}}}) +* 4 fetch (body[] {{{ +Lookslike: header + +Another body +body4 + +}}}) +* 5 fetch (body[] {{{ +From: new@example.com +Subject: test header + +Hello world body + +suffix + +}}}) +* 6 fetch (body[] {{{ +From: hdr1@example.com +Subject: hdr2 + +body6 + +}}}) +* 7 fetch (body[] {{{ +Hdr: foo + +body7 + +}}}) + + +# +# Try appending to nonexistent mailbox +# + +append ${mailbox}nonexistent catenate (url "/$mailbox_url/;uid=$uid/;section=1") +no [trycreate] + +append ${mailbox}nonexistent catenate (url "/$mailbox_url/;uid=$uid/;section=1" text {{{ +hello +}}}) +no [trycreate] + +append ${mailbox}nonexistent catenate (text {{{ +hello +}}} url "/$mailbox_url/;uid=$uid/;section=1") +no [trycreate] + +append ${mailbox}nonexistent (\seen \flagged) catenate (text {{{ +hdr1: 1 + +}}} url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header" text {{{ +hdr2: 2 + +}}} url "/$mailbox_url/;uid=$uid/;section=header" text {{{ +hdr3: 3 + +}}}) +no [trycreate] + +ok noop diff --git a/tests/resources/imap-test/catenate-multiappend b/tests/resources/imap-test/catenate-multiappend new file mode 100644 index 00000000..0733e2b2 --- /dev/null +++ b/tests/resources/imap-test/catenate-multiappend @@ -0,0 +1,130 @@ +state: created +capabilities: CATENATE MULTIAPPEND + +ok append $mailbox catenate (text {{{ +From: foo@example.com + +Hello world body + +}}}) catenate (text {{{ +From: bar@example.com + +Second body + +}}}) + +ok select $mailbox +* ok [uidvalidity $uidv] + +ok fetch 1:2 (uid body.peek[]) +* 1 fetch (uid $uid body[] {{{ +From: foo@example.com + +Hello world body + +}}}) +* 2 fetch (uid $uid2 body[] {{{ +From: bar@example.com + +Second body + +}}}) + +ok append ${mailbox} catenate (url "/$mailbox_url/;uid=$uid/;section=header" text {{{ +body1 + +}}}) catenate (url "/$mailbox_url/;uid=$uid2/;section=header" text {{{ +body2 + +}}}) + +ok append ${mailbox} catenate (url "/$mailbox_url/;uid=$uid") catenate (url "/$mailbox_url/;uid=$uid2/;section=header" text {{{ +body3 + +}}}) + +ok append ${mailbox} catenate (url "/$mailbox_url/;uid=$uid") {{{ +New: Message + +body4 + +}}} + +ok append ${mailbox} catenate (url "/$mailbox_url/;uid=$uid") (\answered) {{{ +New: Message + +body5 + +}}} + +ok noop + +ok fetch 3:* body.peek[] +* 3 fetch (body[] {{{ +From: foo@example.com + +body1 + +}}}) +* 4 fetch (body[] {{{ +From: bar@example.com + +body2 + +}}}) +* 5 fetch (body[] {{{ +From: foo@example.com + +Hello world body + +}}}) +* 6 fetch (body[] {{{ +From: bar@example.com + +body3 + +}}}) +* 7 fetch (body[] {{{ +From: foo@example.com + +Hello world body + +}}}) +* 8 fetch (body[] {{{ +New: Message + +body4 + +}}}) +* 9 fetch (body[] {{{ +From: foo@example.com + +Hello world body + +}}}) +* 10 fetch (body[] {{{ +New: Message + +body5 + +}}}) + +# +# Try invalid URLs +# + +no append ${mailbox} (\seen \flagged) catenate (url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header") catenate (url "/$mailbox_url/;uid=$uid") + +no append ${mailbox} (\seen \flagged) catenate (url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header") catenate (url "/$mailbox_url/;uid=$uid" text {{{ +hello +}}}) + +no append ${mailbox} (\seen \flagged) catenate (url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header") catenate (text {{{ +hello +}}}) + +no append ${mailbox} (\seen \flagged) catenate (url "/$mailbox_url;uidvalidity=12345/;uid=$uid/;section=header") {{{ +hello +}}} + +ok noop diff --git a/tests/resources/imap-test/close b/tests/resources/imap-test/close new file mode 100644 index 00000000..312239bb --- /dev/null +++ b/tests/resources/imap-test/close @@ -0,0 +1,9 @@ +connections: 2 +messages: 3 + +1 ok store 1,3 +flags \deleted +1 ok close + +2 ok noop +* $1 expunge +* $3 expunge diff --git a/tests/resources/imap-test/copy b/tests/resources/imap-test/copy new file mode 100644 index 00000000..e3ec9cde --- /dev/null +++ b/tests/resources/imap-test/copy @@ -0,0 +1,48 @@ +connections: 2 +state: created + +1 ok select ${mailbox} +1 ok append +1 ok append +1 ok append +1 ok append +1 ok append +# make sure the server sees the appended messages +1 ok check + +2 "" delete ${mailbox}2 +2 ok create ${mailbox}2 +2 ok select ${mailbox}2 + +1 ok store 1 flags (\seen) +1 ok store 2 flags (\answered \flagged) +1 ok store 5 flags (\flagged $$keyword1 $$keyword2) + +1 ok fetch 1:5 (internaldate) +* 1 fetch (internaldate $date1) +* 2 fetch (internaldate $date2) +* 4 fetch (internaldate $date4) +* 5 fetch (internaldate $date5) +1 ok copy 1:2,4 ${mailbox}2 + +2 ok noop +* 3 exists +#* 3 recent + +2 ok fetch 1:3 (flags internaldate) +* 3 fetch (flags (\seen) internaldate $date1) +* 2 fetch (flags (\answered \flagged) internaldate $date2) +* 1 fetch (flags () internaldate $date4) + +# keywords aren't required to be created on COPY, so help the server here +2 ok store 3 +flags ($$keyword1 $$keyword2) + +1 ok copy 5 ${mailbox}2 +2 ok noop +* 4 exists + +2 ok fetch 4 (flags internaldate) +* 4 fetch (flags (\flagged $$keyword1 $$keyword2) internaldate $date5) + +2 ok close +2 "" delete ${mailbox}2 diff --git a/tests/resources/imap-test/default.mbox b/tests/resources/imap-test/default.mbox new file mode 100644 index 00000000..38ae3c68 --- /dev/null +++ b/tests/resources/imap-test/default.mbox @@ -0,0 +1,42 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user1@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s1 + +body1 + +From user@domain Fri Feb 22 17:06:23 2008 +From: user2@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s22 + +body22 + +From user@domain Fri Feb 22 17:06:23 2008 +From: user3@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s333 + +body33 + +From user@domain Fri Feb 22 17:06:23 2008 +From: user4@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s4444 + +body4444 + +From user@domain Fri Feb 22 17:06:23 2008 +From: user5@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s55555 + +body55555 + +From user@domain Fri Feb 22 17:06:23 2008 +From: user6@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s666666 + +body666666 + diff --git a/tests/resources/imap-test/esearch b/tests/resources/imap-test/esearch new file mode 100644 index 00000000..914479ac --- /dev/null +++ b/tests/resources/imap-test/esearch @@ -0,0 +1,76 @@ +capabilities: ESEARCH +messages: all + +ok store 4 +flags \deleted +ok expunge + +# SEARCH ALL + +ok search return (all) all +* esearch (tag $tag) all 1:6 +ok search return () all +* esearch (tag $tag) all 1:6 +ok search return (min) all +* esearch (tag $tag) min 1 +ok search return (max) all +* esearch (tag $tag) max 6 +ok search return (count) all +* esearch (tag $tag) count 6 + +# UID SEARCH ALL + +ok fetch 1:* UID +* 1 fetch (uid $uid1) +* 2 fetch (uid $uid2) +* 3 fetch (uid $uid3) +* 4 fetch (uid $uid4) +* 5 fetch (uid $uid5) +* 6 fetch (uid $uid6) + +ok uid search return (all) all +* esearch (tag $tag) uid all $uid1:$uid3,$uid4:$uid6 +ok uid search return () all +* esearch (tag $tag) uid all $uid1:$uid3,$uid4:$uid6 +ok uid search return (min) all +* esearch (tag $tag) uid min $uid1 +ok uid search return (max) all +* esearch (tag $tag) uid max $uid6 +ok uid search return (count) all +* esearch (tag $tag) uid count 6 + +# \Seen flag test + +ok store 2,4 +flags \seen + +ok uid search return (all) seen +* esearch (tag $tag) uid all $uid2,$uid4 +ok uid search return () seen +* esearch (tag $tag) uid all $uid2,$uid4 +ok uid search return (min) seen +* esearch (tag $tag) uid min $uid2 +ok uid search return (max) seen +* esearch (tag $tag) uid max $uid4 +ok uid search return (count) seen +* esearch (tag $tag) uid count 2 + +# nonexistent + +ok search return () 1000 +* esearch (tag $tag) +ok search return (min) 1000 +* esearch (tag $tag) +ok search return (max) 1000 +* esearch (tag $tag) +ok search return (count) 1000 +* esearch (tag $tag) count 0 + +# UID nonexistent + +ok uid search return () 1000 +* esearch (tag $tag) uid +ok uid search return (min) 1000 +* esearch (tag $tag) uid +ok uid search return (max) 1000 +* esearch (tag $tag) uid +ok uid search return (count) 1000 +* esearch (tag $tag) uid count 0 diff --git a/tests/resources/imap-test/esearch-condstore b/tests/resources/imap-test/esearch-condstore new file mode 100644 index 00000000..c2c2ae68 --- /dev/null +++ b/tests/resources/imap-test/esearch-condstore @@ -0,0 +1,38 @@ +capabilities: ESEARCH CONDSTORE +messages: 4 + +# ENABLE is valid only in authenticated state. we could do enable+select +# manually here also, but lets just make sure that switching it on via +# condstore-enabling command works as well (since that is valid) +ok fetch 1 modseq +* ok [highestmodseq $highestmodseq] + +ok store 1 +flags \seen +* 1 fetch (modseq ($modseq1)) +ok store 3 +flags \seen +* 3 fetch (modseq ($modseq3)) +ok store 2 +flags \seen +* 2 fetch (modseq ($modseq2)) +ok store 4 +flags \seen +* 4 fetch (modseq ($modseq4)) + +ok search return (min) 1:3 modseq "/flags/\\seen" all $highestmodseq +* esearch (tag $tag) min 1 modseq $modseq4 + +ok search return (max) 1:3 modseq "/flags/\\seen" all $highestmodseq +* esearch (tag $tag) max 3 modseq $modseq4 + +ok search return () 1:3 modseq "/flags/\\seen" all $highestmodseq +* esearch (tag $tag) all 1:3 modseq $modseq4 + +ok search return (min max) 2:3 modseq "/flags/\\seen" all $highestmodseq +* esearch (tag $tag) min 2 max 3 modseq $modseq4 + +ok search return (all) 2:4 modseq "/flags/\\seen" all $modseq3 +* esearch (tag $tag) all 2,4 modseq $modseq4 + +ok search return (all) 2:4 modseq "/flags/\\seen" all $modseq2 +* esearch (tag $tag) all 4 modseq $modseq4 + +ok search return (all) 2:4 modseq "/flags/\\seen" all $modseq4 +* esearch (tag $tag) modseq $modseq4 diff --git a/tests/resources/imap-test/esearch.mbox b/tests/resources/imap-test/esearch.mbox new file mode 100644 index 00000000..a82ff915 --- /dev/null +++ b/tests/resources/imap-test/esearch.mbox @@ -0,0 +1,28 @@ +From user@domain Fri Feb 22 17:06:21 2008 + +1 + +From user@domain Fri Feb 22 17:06:22 2008 + +2 + +From user@domain Fri Feb 22 17:06:23 2008 + +3 + +From user@domain Fri Feb 22 17:06:23 2008 + +4 + +From user@domain Fri Feb 22 17:06:25 2008 + +5 + +From user@domain Fri Feb 22 17:06:25 2008 + +6 + +From user@domain Fri Feb 22 17:06:25 2008 + +7 + diff --git a/tests/resources/imap-test/esort b/tests/resources/imap-test/esort new file mode 100644 index 00000000..cade9d65 --- /dev/null +++ b/tests/resources/imap-test/esort @@ -0,0 +1,11 @@ +capabilities: ESORT +messages: 5 + +ok sort return (min) (arrival) us-ascii 1:5 +* esearch (tag $tag) min 1 +ok sort return (max) (arrival) us-ascii 1:5 +* esearch (tag $tag) max 5 +ok sort return (all) (arrival) us-ascii 1:5 +* esearch (tag $tag) all 1:5 +ok sort return (count) (arrival) us-ascii 1:5 +* esearch (tag $tag) count 5 diff --git a/tests/resources/imap-test/expunge b/tests/resources/imap-test/expunge new file mode 100644 index 00000000..bf64433a --- /dev/null +++ b/tests/resources/imap-test/expunge @@ -0,0 +1,62 @@ +connections: 2 +messages: 8 + +# get UIDs +1 ok fetch 1:4 uid +* 1 fetch (uid $uid1) +* 2 fetch (uid $uid2) +* 3 fetch (uid $uid3) +* 4 fetch (uid $uid4) + +# 1) test that expunges work ok and session 2 fetch sees 1's flag changes. +1 ok store 1,3 flags \deleted +* 1 fetch (flags (\deleted)) +* 3 fetch (flags (\deleted)) +1 ok store 2,4 flags \seen +* 2 fetch (flags (\seen)) +* 4 fetch (flags (\seen)) +1 ok expunge +* $1 expunge +* $3 expunge + +2 ok fetch 2,4 (uid) +* 2 fetch (uid $uid2) +* 4 fetch (uid $uid4) +2 ok fetch 2,4 (uid) +* 2 fetch (uid $uid2) +* 4 fetch (uid $uid4) +2 ok check +* $1 expunge +* $3 expunge +2 ok fetch 1:2 (uid flags) +* 1 fetch (uid $uid2 flags (\seen)) +* 2 fetch (uid $uid4 flags (\seen)) + +# 2) test that session 2 can update flags while some messages are expunged +1 ok store 2 +flags \deleted +1 ok expunge + +2 ok store 1 flags \answered +* 1 fetch (flags (\answered)) +1 ok check +* 1 fetch (flags (\answered)) +2 ok noop +* 2 expunge + +# 3) check notices flag changes correctly with expunges +1 ok store 1,3 +flags \deleted +1 ok store 2,4 flags \flagged +1 ok expunge +2 ok check +* $1 expunge +* $2 fetch (flags (\flagged)) +* $3 expunge +* $4 fetch (flags (\flagged)) + +# 4) expunging while message is already expunged +1 ok store 1 +flags \deleted +2 ok store 1 +flags \deleted +1 ok expunge +* 1 expunge +2 ok expunge +* 1 expunge diff --git a/tests/resources/imap-test/expunge2 b/tests/resources/imap-test/expunge2 new file mode 100644 index 00000000..e2975a84 --- /dev/null +++ b/tests/resources/imap-test/expunge2 @@ -0,0 +1,44 @@ +connections: 2 +messages: 6 + +1 ok fetch 2,4 uid +* 2 fetch (uid $uid2) +* 4 fetch (uid $uid4) + +# UID FETCH +1 ok store 1,3 +flags \deleted +1 ok expunge +* $1 expunge +* $3 expunge + +2 ok uid fetch $uid2,$uid4 uid +* $2 fetch (uid $uid2) +* $4 fetch (uid $uid4) +2 ok noop + +# UID STORE +1 ok store 1 +flags \deleted +1 ok expunge +* 1 expunge + +2 ok uid store $uid4 flags \seen +* $2 fetch (uid $uid4 flags (\seen)) +2 ok noop + +# Make sure CHECK works just as well as NOOP +1 ok store 1 +flags \deleted +1 ok expunge +* 1 expunge +2 ok check +* 1 expunge + +# Make sure FETCH, STORE and SEARCH don't trigger EXPUNGE +1 ok store 1 +flags \deleted +1 ok expunge + +2 ok fetch 2 flags +! $1 expunge +2 ok store 2 flags (\seen) +! $1 expunge +2 ok search all +! $1 expunge diff --git a/tests/resources/imap-test/fetch b/tests/resources/imap-test/fetch new file mode 100644 index 00000000..6c9d23a6 --- /dev/null +++ b/tests/resources/imap-test/fetch @@ -0,0 +1,54 @@ +messages: 3 + +ok status $mailbox (uidnext) +* status $mailbox (uidnext $uidnext) + +ok fetch 1:3 uid +* 1 fetch (uid $uid1) +* 2 fetch (uid $uid2) +* 3 fetch (uid $uid3) + +ok fetch 3:1 uid +* 1 fetch (uid $uid1) +* 2 fetch (uid $uid2) +* 3 fetch (uid $uid3) + +ok fetch 1,* uid +* 1 fetch (uid $uid1) +* 3 fetch (uid $uid3) + +ok fetch * uid +* 3 fetch (uid $uid3) + +ok fetch *:1 uid +* 1 fetch (uid $uid1) +* 2 fetch (uid $uid2) +* 3 fetch (uid $uid3) + +ok uid fetch $uidnext:* uid +* 3 fetch (uid $uid3) + +# break seq=uid map +ok store 2 flags \deleted +ok expunge +* 2 expunge + +ok uid fetch $uidnext:* uid +* 2 fetch (uid $uid3) +ok uid fetch 1:* uid +* 1 fetch (uid $uid1) +* 2 fetch (uid $uid3) +ok uid fetch $uid2 uid +! 1 fetch (uid $uid1) +! 2 fetch (uid $uid3) +ok uid fetch $uid1,* uid +* 1 fetch (uid $uid1) +* 2 fetch (uid $uid3) + +# test macros +ok fetch 1 full +* 1 fetch (flags () internaldate $intdate1 rfc822.size $size1 envelope ($!unordered) body ($!unordered)) +ok fetch 1 all +* 1 fetch (flags () internaldate $intdate1 rfc822.size $size1 envelope ($!unordered)) +ok fetch 1 fast +* 1 fetch (flags () internaldate $intdate1 rfc822.size $size1) diff --git a/tests/resources/imap-test/fetch-binary-mime b/tests/resources/imap-test/fetch-binary-mime new file mode 100644 index 00000000..eace1a1d --- /dev/null +++ b/tests/resources/imap-test/fetch-binary-mime @@ -0,0 +1,104 @@ +capabilities: BINARY +messages: all + +# This is the fetch-body-mime test, except with body[] changed to binary[]. +# The idea is to verify that binary[] works correctly when it doesn't actually +# have to convert anything. + +ok fetch 1 (binary.peek[]) +* 1 fetch (binary[] {{{ +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + +Root MIME prologue + +--foo bar +Content-Type: text/x-myown; charset=us-ascii + +hello + +--foo bar +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +--foo bar-- +Root MIME epilogue + +}}}) + +ok fetch 1 (binary.size[]) +* 1 fetch (binary.size[] 602) + +ok fetch 1 (binary.size[1]) +* 1 fetch (binary.size[1] 7) + +ok fetch 1 (binary.peek[1]) +* 1 fetch (binary[1] {{{ +hello + +}}}) + +ok fetch 1 (binary.peek[2]) +* 1 fetch (binary[2] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +}}}) + +ok fetch 1 (binary.size[2]) +* 1 fetch (binary.size[2] 298) + +ok fetch 1 (binary.size[2.1]) +* 1 fetch (binary.size[2.1] 20) + +ok fetch 1 (binary.peek[2.1]) +* 1 fetch (binary[2.1] {{{ +

Hello world

+ +}}}) + +ok fetch 1 (binary.peek[2.2]) +* 1 fetch (binary[2.2] {{{ +Hello another world + +}}}) + +ok fetch 1 (binary.size[2.2]) +* 1 fetch (binary.size[2.2] 21) diff --git a/tests/resources/imap-test/fetch-binary-mime-base64 b/tests/resources/imap-test/fetch-binary-mime-base64 new file mode 100644 index 00000000..9a7c0971 --- /dev/null +++ b/tests/resources/imap-test/fetch-binary-mime-base64 @@ -0,0 +1,67 @@ +capabilities: BINARY +messages: all + +ok fetch 1 (binary.size[1] binary.size[5]) +* 1 fetch (binary.size[1] 11 binary.size[5] 30) + +ok fetch 1 (binary.peek[1] binary.peek[2] binary.peek[3] binary.peek[4] binary.peek[5]) +* 1 fetch (binary[1] {{{ +hello world +}}} binary[2] {{{ +hello world +}}} binary[3] {{{ +hello to you too +}}} binary[4] {{{ +hello to everyone! +}}} binary[5] ~{{{ +abcdefg hijkl +mno +pqrstuvqxyz +}}}) + +ok fetch 1 (binary.size[1] binary.peek[1]) +* 1 fetch (binary.size[1] 11 binary[1] {{{ +hello world +}}}) + +ok fetch 1 (binary.size[3] binary.peek[2]) +* 1 fetch (binary.size[3] 16 binary[2] {{{ +hello world +}}}) + +ok fetch 1 (binary.size[2] binary.size[3] binary.size[4]) +* 1 fetch (binary.size[2] 11 binary.size[3] 16 binary.size[4] 18) + +ok fetch 1 (binary.peek[5]<0.7>) +* 1 fetch (binary[5]<0> ~{{{ +abcdefg +}}}) + +ok fetch 1 (binary.peek[5]<0.8>) +* 1 fetch (binary[5]<0> ~{{{ +abcdefg +}}}) + +ok fetch 1 (binary.peek[5]<0.10>) +* 1 fetch (binary[5]<0> ~{{{ +abcdefg hi +}}}) + +ok fetch 1 (binary.peek[5]<10.10>) +* 1 fetch (binary[5]<10> ~{{{ +jkl +mno +p +}}}) + +ok fetch 1 (binary.peek[5]<5.10>) +* 1 fetch (binary[5]<5> ~{{{ +fg hijkl + +}}}) + +ok fetch 1 (binary.peek[5]<15.100>) +* 1 fetch (binary[5]<15> ~{{{ +mno +pqrstuvqxyz +}}}) diff --git a/tests/resources/imap-test/fetch-binary-mime-base64.mbox b/tests/resources/imap-test/fetch-binary-mime-base64.mbox new file mode 100644 index 00000000..611da2fc --- /dev/null +++ b/tests/resources/imap-test/fetch-binary-mime-base64.mbox @@ -0,0 +1,49 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + +Root MIME prologue + +--foo bar +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: base64 + + +aGVs +bG8gd29y +bGQ= + +--foo bar +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: base64 + + aGVs + bG8g + d29y + bGQ= + + +--foo bar +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: base64 + +aGVsbG8g +dG8geW91IHRv +bw== + +--foo bar +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: base64 + +aGVsbG8gdG8gZXZlcnlvbmUh +--foo bar +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: base64 + +YWJjZGVmZw1oaWprbA0KbW5vCnBxcnN0dXZxeHl6 + +--foo bar-- +Root MIME epilogue diff --git a/tests/resources/imap-test/fetch-binary-mime-qp b/tests/resources/imap-test/fetch-binary-mime-qp new file mode 100644 index 00000000..728300d2 --- /dev/null +++ b/tests/resources/imap-test/fetch-binary-mime-qp @@ -0,0 +1,54 @@ +capabilities: BINARY +messages: all + +ok fetch 1 (binary.size[1]) +* 1 fetch (binary.size[1] 65) + +ok fetch 1 (binary.peek[1]) +* 1 fetch (binary[1] {{{ +hello +bar +foo bar +foo b +foo bar +foo b +foo +bar +foo_bar + +}}}) + +ok fetch 1 (binary.peek[1]<0.10>) +* 1 fetch (binary[1]<0> {{{ +hello +bar +}}}) + +ok fetch 1 (binary.peek[1]<10.10>) +* 1 fetch (binary[1]<10> ~{{{ + +foo bar +}}}) + +ok fetch 1 (binary.peek[1]<20.10>) +* 1 fetch (binary[1]<20> ~{{{ + +foo b + +}}}) + +ok fetch 1 (binary.peek[1]<15.10>) +* 1 fetch (binary[1]<15> ~{{{ + bar +foo +}}}) + +ok fetch 1 (binary.peek[1]<40.100>) +* 1 fetch (binary[1]<40> {{{ +oo b +foo +bar +foo_bar + +}}}) + diff --git a/tests/resources/imap-test/fetch-binary-mime-qp.mbox b/tests/resources/imap-test/fetch-binary-mime-qp.mbox new file mode 100644 index 00000000..52954d58 --- /dev/null +++ b/tests/resources/imap-test/fetch-binary-mime-qp.mbox @@ -0,0 +1,31 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + +Root MIME prologue + +--foo bar +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: quoted-printable + +hello +bar= + +foo = +bar +foo = +=62 +foo = +bar +foo = +=62 +foo +bar= + +foo_bar + +--foo bar-- +Root MIME epilogue diff --git a/tests/resources/imap-test/fetch-binary-mime.mbox b/tests/resources/imap-test/fetch-binary-mime.mbox new file mode 100644 index 00000000..e219079c --- /dev/null +++ b/tests/resources/imap-test/fetch-binary-mime.mbox @@ -0,0 +1,38 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + +Root MIME prologue + +--foo bar +Content-Type: text/x-myown; charset=us-ascii + +hello + +--foo bar +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +--foo bar-- +Root MIME epilogue diff --git a/tests/resources/imap-test/fetch-body b/tests/resources/imap-test/fetch-body new file mode 100644 index 00000000..d08ba645 --- /dev/null +++ b/tests/resources/imap-test/fetch-body @@ -0,0 +1,122 @@ +messages: all + +# header and body fetches +ok fetch 1 rfc822.header +* 1 fetch (rfc822.header $hdr1) +ok fetch 1 body.peek[header] +* 1 fetch (body[header] $hdr1) + +ok fetch 1 (flags body.peek[text]) +* 1 fetch (flags () body[text] {{{ +body1 + + +}}}) +ok fetch 1 rfc822.text +* 1 fetch (rfc822.text {{{ +body1 + + +}}}) +* 1 fetch (flags (\seen)) + +ok fetch 2 (flags body.peek[]) +* 2 fetch (flags () body[] $full2) +ok fetch 2 rfc822 +* 2 fetch (rfc822 $full2) +* 2 fetch (flags (\seen)) + +ok fetch 3 (body[]) +* 3 fetch (body[] $full3) +* 3 fetch (flags (\seen)) +ok fetch 4 (body[header]) +* 4 fetch (body[header] $hdr4) +* 4 fetch (flags (\seen)) + +# partial fetches +ok fetch 2 body.peek[text]<0.3> +* 2 fetch (body[text]<0> "bod") +ok fetch 2 body.peek[text]<3.3> +* 2 fetch (body[text]<3> "y22") + +ok fetch 3 body.peek[text]<0.1> +* 3 fetch (body[text]<0> "b") +ok fetch 3 body.peek[text]<5.1> +* 3 fetch (body[text]<5> "3") +ok fetch 3 body.peek[text]<5.2> +* 3 fetch (body[text]<5> ~{{{ +3 +}}}) +ok fetch 3 body.peek[text]<5.3> +* 3 fetch (body[text]<5> ~{{{ +3 + +}}}) +ok fetch 3 body.peek[text]<6.1> +* 3 fetch (body[text]<6> ~{{{ + +}}}) +ok fetch 3 body.peek[text]<6.2> +* 3 fetch (body[text]<6> ~{{{ + + +}}}) +ok fetch 3 body.peek[text]<7.1> +* 3 fetch (body[text]<7> ~{{{ + + +}}}) + +# header fields +ok fetch 1 body.peek[header.fields (from)] +* 1 fetch (body[header.fields (from)] {{{ +From: User1 + + +}}}) + +ok fetch 1 (body.peek[header.fields (from)]) +* 1 fetch (body[header.fields (from)] {{{ +From: User1 + + +}}}) + +ok fetch 1 (body.peek[header.fields (from from)]) +* 1 fetch (body[header.fields (from from)] {{{ +From: User1 + + +}}}) + +ok fetch 1 body.peek[header.fields (from subject)] +* 1 fetch (body[header.fields (from subject)] {{{ +From: User1 +Subject: s1 + + +}}}) + +ok fetch 1 body.peek[header.fields.not (date)] +! 1 fetch (body[header.fields.not (date)] {{{ +From: User1 +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s1 + + +}}}) + +ok fetch 1 body.peek[header.fields.not (date date)] +! 1 fetch (body[header.fields.not (date date)] {{{ +From: User1 +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s1 + + +}}}) + +ok fetch 1 body.peek[header.fields (xyz)] +* 1 fetch (body[header.fields (xyz)] {{{ + + +}}}) diff --git a/tests/resources/imap-test/fetch-body-message-rfc822 b/tests/resources/imap-test/fetch-body-message-rfc822 new file mode 100644 index 00000000..77d4110d --- /dev/null +++ b/tests/resources/imap-test/fetch-body-message-rfc822 @@ -0,0 +1,73 @@ +messages: all + +ok fetch 1 (body.peek[]) +* 1 fetch (body[] {{{ +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +}}}) + +ok fetch 1 (body.peek[text]) +* 1 fetch (body[text] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +}}}) + +ok fetch 1 (body.peek[1]) +* 1 fetch (body[1] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +}}}) + +ok fetch 1 (body.peek[1.1]) +* 1 fetch (body[1.1] {{{ +Hello world + +}}}) + +ok fetch 1 (body.peek[1.header]) +* 1 fetch (body[1.header] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + + +}}}) + +ok fetch 1 (body.peek[1.header.fields (from subject X-foo)]) +* 1 fetch (body[1.header.fields (from subject X-foo)] {{{ +From: sub@domain.org +Subject: submsg + + +}}}) + +ok fetch 1 (body.peek[1.header.fields.not (from subject X-foo)]) +* 1 fetch (body[1.header.fields.not (from subject X-foo)] {{{ +Date: Sun, 12 Aug 2012 12:34:56 +0300 + + +}}}) + +ok fetch 1 (body.peek[1.text]) +* 1 fetch (body[1.text] {{{ +Hello world + +}}}) + diff --git a/tests/resources/imap-test/fetch-body-message-rfc822-mime b/tests/resources/imap-test/fetch-body-message-rfc822-mime new file mode 100644 index 00000000..8db99da9 --- /dev/null +++ b/tests/resources/imap-test/fetch-body-message-rfc822-mime @@ -0,0 +1,191 @@ +messages: all + +ok fetch 1 (body.peek[]) +* 1 fetch (body[] {{{ +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/digest; boundary="foo" + +prologue + +--foo + +From: m1@example.com +Subject: m1 + +m1 body + +--foo +X-Mime: m2 header + +From: m2@example.com +Subject: m2 + +m2 body + +--foo-- + +epilogue + +}}}) + +ok fetch 1 (body.peek[text]) +* 1 fetch (body[text] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/digest; boundary="foo" + +prologue + +--foo + +From: m1@example.com +Subject: m1 + +m1 body + +--foo +X-Mime: m2 header + +From: m2@example.com +Subject: m2 + +m2 body + +--foo-- + +epilogue + +}}}) + +ok fetch 1 (body.peek[1]) +* 1 fetch (body[1] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/digest; boundary="foo" + +prologue + +--foo + +From: m1@example.com +Subject: m1 + +m1 body + +--foo +X-Mime: m2 header + +From: m2@example.com +Subject: m2 + +m2 body + +--foo-- + +epilogue + +}}}) + +ok fetch 1 (body.peek[1.header]) +* 1 fetch (body[1.header] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/digest; boundary="foo" + + +}}}) + +ok fetch 1 (body.peek[1.text]) +* 1 fetch (body[1.text] {{{ +prologue + +--foo + +From: m1@example.com +Subject: m1 + +m1 body + +--foo +X-Mime: m2 header + +From: m2@example.com +Subject: m2 + +m2 body + +--foo-- + +epilogue + +}}}) + +ok fetch 1 (body.peek[1.1]) +* 1 fetch (body[1.1] {{{ +From: m1@example.com +Subject: m1 + +m1 body + +}}}) + +ok fetch 1 (body.peek[1.1.MIME]) +* 1 fetch (body[1.1.MIME] {{{ + + +}}}) + +ok fetch 1 (body.peek[1.1.HEADER]) +* 1 fetch (body[1.1.HEADER] {{{ +From: m1@example.com +Subject: m1 + + +}}}) + +ok fetch 1 (body.peek[1.1.TEXT]) +* 1 fetch (body[1.1.TEXT] {{{ +m1 body + +}}}) + +ok fetch 1 (body.peek[1.2]) +* 1 fetch (body[1.2] {{{ +From: m2@example.com +Subject: m2 + +m2 body + +}}}) + +ok fetch 1 (body.peek[1.2.MIME]) +* 1 fetch (body[1.2.MIME] {{{ +X-Mime: m2 header + + +}}}) + +ok fetch 1 (body.peek[1.2.HEADER]) +* 1 fetch (body[1.2.HEADER] {{{ +From: m2@example.com +Subject: m2 + + +}}}) + +ok fetch 1 (body.peek[1.2.TEXT]) +* 1 fetch (body[1.2.TEXT] {{{ +m2 body + +}}}) + diff --git a/tests/resources/imap-test/fetch-body-message-rfc822-mime.mbox b/tests/resources/imap-test/fetch-body-message-rfc822-mime.mbox new file mode 100644 index 00000000..9208343f --- /dev/null +++ b/tests/resources/imap-test/fetch-body-message-rfc822-mime.mbox @@ -0,0 +1,31 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/digest; boundary="foo" + +prologue + +--foo + +From: m1@example.com +Subject: m1 + +m1 body + +--foo +X-Mime: m2 header + +From: m2@example.com +Subject: m2 + +m2 body + +--foo-- + +epilogue diff --git a/tests/resources/imap-test/fetch-body-message-rfc822-x2 b/tests/resources/imap-test/fetch-body-message-rfc822-x2 new file mode 100644 index 00000000..93a0f73c --- /dev/null +++ b/tests/resources/imap-test/fetch-body-message-rfc822-x2 @@ -0,0 +1,103 @@ +messages: all + +ok fetch 1 (body.peek[]) +* 1 fetch (body[] {{{ +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: user2@domain.org +Date: Fri, 23 Mar 2007 11:22:33 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +}}}) + +ok fetch 1 (body.peek[text]) +* 1 fetch (body[text] {{{ +From: user2@domain.org +Date: Fri, 23 Mar 2007 11:22:33 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +}}}) + +ok fetch 1 (body.peek[1]) +* 1 fetch (body[1] {{{ +From: user2@domain.org +Date: Fri, 23 Mar 2007 11:22:33 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +}}}) + +ok fetch 1 (body.peek[1.1]) +* 1 fetch (body[1.1] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +}}}) + +ok fetch 1 (body.peek[1.header]) +* 1 fetch (body[1.header] {{{ +From: user2@domain.org +Date: Fri, 23 Mar 2007 11:22:33 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + + +}}}) + +ok fetch 1 (body.peek[1.text]) +* 1 fetch (body[1.text] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +}}}) + +ok fetch 1 (body.peek[1.1.1]) +* 1 fetch (body[1.1.1] {{{ +Hello world + +}}}) + +ok fetch 1 (body.peek[1.1.header]) +* 1 fetch (body[1.1.header] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + + +}}}) + +ok fetch 1 (body.peek[1.1.text]) +* 1 fetch (body[1.1.text] {{{ +Hello world + +}}}) + diff --git a/tests/resources/imap-test/fetch-body-message-rfc822-x2.mbox b/tests/resources/imap-test/fetch-body-message-rfc822-x2.mbox new file mode 100644 index 00000000..53ebee3a --- /dev/null +++ b/tests/resources/imap-test/fetch-body-message-rfc822-x2.mbox @@ -0,0 +1,16 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: user2@domain.org +Date: Fri, 23 Mar 2007 11:22:33 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world diff --git a/tests/resources/imap-test/fetch-body-message-rfc822.mbox b/tests/resources/imap-test/fetch-body-message-rfc822.mbox new file mode 100644 index 00000000..e0ca1823 --- /dev/null +++ b/tests/resources/imap-test/fetch-body-message-rfc822.mbox @@ -0,0 +1,11 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world diff --git a/tests/resources/imap-test/fetch-body-mime b/tests/resources/imap-test/fetch-body-mime new file mode 100644 index 00000000..ba510dd6 --- /dev/null +++ b/tests/resources/imap-test/fetch-body-mime @@ -0,0 +1,193 @@ +messages: all + +ok fetch 1 (body.peek[]) +* 1 fetch (body[] {{{ +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + +Root MIME prologue + +--foo bar +Content-Type: text/x-myown; charset=us-ascii + +hello + +--foo bar +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +--foo bar-- +Root MIME epilogue + +}}}) + +ok fetch 1 (body.peek[text]) +* 1 fetch (body[text] {{{ +Root MIME prologue + +--foo bar +Content-Type: text/x-myown; charset=us-ascii + +hello + +--foo bar +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +--foo bar-- +Root MIME epilogue + +}}}) + +ok fetch 1 (body.peek[1]) +* 1 fetch (body[1] {{{ +hello + +}}}) + +ok fetch 1 (body.peek[1.mime]) +* 1 fetch (body[1.mime] {{{ +Content-Type: text/x-myown; charset=us-ascii + + +}}}) + +ok fetch 1 (body.peek[2]) +* 1 fetch (body[2] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +}}}) + +ok fetch 1 (body.peek[2.mime]) +* 1 fetch (body[2.mime] {{{ +Content-Type: message/rfc822 + + +}}}) + +ok fetch 1 (body.peek[2.header]) +* 1 fetch (body[2.header] {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + + +}}}) + +ok fetch 1 (body.peek[2.header.fields (from subject X-foo)]) +* 1 fetch (body[2.header.fields (from subject X-foo)] {{{ +From: sub@domain.org +Subject: submsg + + +}}}) + +ok fetch 1 (body.peek[2.header.fields.not (from subject X-foo)]) +* 1 fetch (body[2.header.fields.not (from subject X-foo)] {{{ +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Content-Type: multipart/alternative; boundary="sub1" + + +}}}) + +ok fetch 1 (body.peek[2.text]) +* 1 fetch (body[2.text] {{{ +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +}}}) + +ok fetch 1 (body.peek[2.1.mime]) +* 1 fetch (body[2.1.mime] {{{ +Content-Type: text/html + + +}}}) + +ok fetch 1 (body.peek[2.1]) +* 1 fetch (body[2.1] {{{ +

Hello world

+ +}}}) + +ok fetch 1 (body.peek[2.2.mime]) +* 1 fetch (body[2.2.mime] {{{ +Content-Type: text/plain + + +}}}) + +ok fetch 1 (body.peek[2.2]) +* 1 fetch (body[2.2] {{{ +Hello another world + +}}}) + diff --git a/tests/resources/imap-test/fetch-body-mime.mbox b/tests/resources/imap-test/fetch-body-mime.mbox new file mode 100644 index 00000000..e219079c --- /dev/null +++ b/tests/resources/imap-test/fetch-body-mime.mbox @@ -0,0 +1,38 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + +Root MIME prologue + +--foo bar +Content-Type: text/x-myown; charset=us-ascii + +hello + +--foo bar +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +--foo bar-- +Root MIME epilogue diff --git a/tests/resources/imap-test/fetch-body.mbox b/tests/resources/imap-test/fetch-body.mbox new file mode 100644 index 00000000..b9b66d3f --- /dev/null +++ b/tests/resources/imap-test/fetch-body.mbox @@ -0,0 +1,28 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: User1 +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s1 + +body1 + +From user@domain Fri Feb 22 17:06:23 2008 +From: User2 +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s22 + +body22 + +From user@domain Fri Feb 22 17:06:23 2008 +From: User3 +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s333 + +body33 + +From user@domain Fri Feb 22 17:06:23 2008 +From: User4 +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: s4444 + +body4444 + diff --git a/tests/resources/imap-test/fetch-bodystructure b/tests/resources/imap-test/fetch-bodystructure new file mode 100644 index 00000000..94687803 --- /dev/null +++ b/tests/resources/imap-test/fetch-bodystructure @@ -0,0 +1,4 @@ +messages: all + +ok fetch 1:* body +* 1 FETCH (BODY (("TEXT" "x-MYOWN" ("CHARSET" "us-ascii") NIL NIL "7BIT" 7 1) "MIXED")) diff --git a/tests/resources/imap-test/fetch-bodystructure.mbox b/tests/resources/imap-test/fetch-bodystructure.mbox new file mode 100644 index 00000000..9631b45c --- /dev/null +++ b/tests/resources/imap-test/fetch-bodystructure.mbox @@ -0,0 +1,13 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + +--foo bar +Content-Type: text/x-myown; charset=us-ascii + +hello + +--foo bar-- diff --git a/tests/resources/imap-test/fetch-envelope b/tests/resources/imap-test/fetch-envelope new file mode 100644 index 00000000..17d16a47 --- /dev/null +++ b/tests/resources/imap-test/fetch-envelope @@ -0,0 +1,10 @@ +messages: all + +# date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, message-id +ok fetch 1:* envelope +* 1 FETCH (ENVELOPE ("Thu, 15 Feb 2007 01:02:03 +0200" "subject header" (("From Real" NIL "fromuser" "fromdomain.org")) (("Sender Real" NIL "senderuser" "senderdomain.org")) (("ReplyTo Real" NIL "replytouser" "replytodomain.org")) (("To Real" NIL "touser" "todomain.org")) (("Cc Real" NIL "ccuser" "ccdomain.org")) (("Bcc Real" NIL "bccuser" "bccdomain.org")) "" "")) +* 2 FETCH (ENVELOPE ("Thu, 15 Feb 2007 01:02:03 +0200" NIL ((NIL NIL "user" "domain")) ((NIL NIL "user" "domain")) ((NIL NIL "user" "domain")) NIL NIL NIL NIL NIL)) +* 3 FETCH (ENVELOPE ("Thu, 15 Feb 2007 01:02:03 +0200" NIL ((NIL NIL "user" "domain")) ((NIL NIL "user" "domain")) ((NIL NIL "user" "domain")) NIL NIL NIL NIL NIL)) +* 4 FETCH (ENVELOPE ("Thu, 15 Feb 2007 01:02:03 +0200" NIL (("Real Name" NIL "user" "domain")) (("Real Name" NIL "user" "domain")) (("Real Name" NIL "user" "domain")) ((NIL NIL "group" NIL)(NIL NIL "g1" "d1.org")(NIL NIL "g2" "d2.org")(NIL NIL NIL NIL)(NIL NIL "group2" NIL)(NIL NIL "g3" "d3.org")(NIL NIL NIL NIL)) ((NIL NIL "group" NIL)(NIL NIL NIL NIL)(NIL NIL "group2" NIL)(NIL NIL NIL NIL)) NIL NIL NIL)) +* 5 FETCH (ENVELOPE ("Thu, 15 Feb 2007 01:02:03 +0200" NIL (("Real Name" NIL "user" "domain")) (("Real Name" NIL "user" "domain")) (("Real Name" NIL "user" "domain")) NIL NIL NIL NIL NIL)) +* 6 FETCH (ENVELOPE ("Thu, 15 Feb 2007 01:02:03 +0200" NIL ((NIL "@route" "user" "domain")) ((NIL "@route" "user" "domain")) ((NIL "@route" "user" "domain")) NIL NIL NIL NIL NIL)) diff --git a/tests/resources/imap-test/fetch-envelope.mbox b/tests/resources/imap-test/fetch-envelope.mbox new file mode 100644 index 00000000..c70a7db4 --- /dev/null +++ b/tests/resources/imap-test/fetch-envelope.mbox @@ -0,0 +1,47 @@ +From user@domain Fri Feb 22 17:06:23 2008 +Message-ID: +In-Reply-To: +Date: Thu, 15 Feb 2007 01:02:03 +0200 +Subject: subject header +From: From Real +To: To Real +Cc: Cc Real +Bcc: Bcc Real +Sender: Sender Real +Reply-To: ReplyTo Real + +body + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Thu, 15 Feb 2007 01:02:03 +0200 +From: user@domain + +body + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Thu, 15 Feb 2007 01:02:03 +0200 +From: user@domain + +body + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Thu, 15 Feb 2007 01:02:03 +0200 +From: user@domain (Real Name) +To: group: g1@d1.org, g2@d2.org;, group2: g3@d3.org; +Cc: group:;, group2: (foo) ; + +body + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Thu, 15 Feb 2007 01:02:03 +0200 +From: user@domain (Real Name) +Sender: +Reply-To: + +body + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Thu, 15 Feb 2007 01:02:03 +0200 +From: <@route:user@domain> + +body diff --git a/tests/resources/imap-test/id b/tests/resources/imap-test/id new file mode 100644 index 00000000..b34ca5d6 --- /dev/null +++ b/tests/resources/imap-test/id @@ -0,0 +1,4 @@ +capabilities: id +state: nonauth + +ok ID ("a23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "b23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "c23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "d23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "e23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "f23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "g23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "h23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "i23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "j23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "k23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "l23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "m23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "n23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "o23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "p23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "q23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "r23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "s23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "t23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "u23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "v23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "w23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "x23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "y23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "z23456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "aa3456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "ab3456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "ac3456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234" "ad3456789012345678901234567890" "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234") diff --git a/tests/resources/imap-test/list b/tests/resources/imap-test/list new file mode 100644 index 00000000..5e305993 --- /dev/null +++ b/tests/resources/imap-test/list @@ -0,0 +1,79 @@ +connections: 3 +state: auth + +# get the separator +1 ok list "" "" +* list () $sep $root +# it should be \noselect, but don't fail everything if it doesn't exist +* list (\noselect) $sep $root + +1 ok create $mailbox${sep} +1 ok create $mailbox${sep}test +2 ok list "" $mailbox${sep}% +* list () $sep $mailbox${sep}test + +2 ok create $mailbox${sep}test2 +1 ok list "" $mailbox${sep}% +* list () $sep $mailbox${sep}test +* list () $sep $mailbox${sep}test2 + +3 ok create $mailbox${sep}test3${sep} +2 ok create $mailbox${sep}test3${sep}test4${sep} +2 ok create $mailbox${sep}test3${sep}test4${sep}test5 +2 ok list "" $mailbox${sep}% +* list () $sep $mailbox${sep}test +* list () $sep $mailbox${sep}test2 +* list () $sep $mailbox${sep}test3 +! list () $sep $mailbox${sep}test3${sep}test4 +! list () $sep $mailbox${sep}test3${sep}test4${sep}test5 + +3 ok list "" $mailbox${sep}% +* list () $sep $mailbox${sep}test +* list () $sep $mailbox${sep}test2 +* list () $sep $mailbox${sep}test3 + +2 ok list "" $mailbox${sep}%${sep}% +! list () $sep $mailbox${sep}test +! list () $sep $mailbox${sep}test2 +! list () $sep $mailbox${sep}test3 +* list () $sep $mailbox${sep}test3${sep}test4 +! list () $sep $mailbox${sep}test3${sep}test4${sep}test5 + +3 ok list "" $mailbox${sep}* +* list () $sep $mailbox${sep}test +* list () $sep $mailbox${sep}test2 +* list () $sep $mailbox${sep}test3 +* list () $sep $mailbox${sep}test3${sep}test4${sep}test5 + +3 ok list $mailbox${sep} * +* list () $sep $mailbox${sep}test +* list () $sep $mailbox${sep}test2 +* list () $sep $mailbox${sep}test3 +* list () $sep $mailbox${sep}test3${sep}test4${sep}test5 + +2 ok list "" $mailbox${sep}*test4 +! list () $sep $mailbox${sep}test +! list () $sep $mailbox${sep}test2 +! list () $sep $mailbox${sep}test3 +* list () $sep $mailbox${sep}test3${sep}test4 +! list () $sep $mailbox${sep}test3${sep}test4${sep}test5 + +2 ok list "" $mailbox${sep}*test* +* list () $sep $mailbox${sep}test +* list () $sep $mailbox${sep}test2 +* list () $sep $mailbox${sep}test3 +* list () $sep $mailbox${sep}test3${sep}test4${sep}test5 + +2 ok list "" $mailbox${sep}%3${sep}% +* list () $sep $mailbox${sep}test3${sep}test4 +2 ok list "" $mailbox${sep}%3${sep}%4 +* list () $sep $mailbox${sep}test3${sep}test4 +2 ok list "" $mailbox${sep}%t*4 +* list () $sep $mailbox${sep}test3${sep}test4 + +2 ok delete $mailbox${sep}test2 +1 ok list "" $mailbox${sep}* +! list () $sep $mailbox${sep}test2 + +1 ok list "" INBOX +* list () $inboxsep INBOX diff --git a/tests/resources/imap-test/listext b/tests/resources/imap-test/listext new file mode 100644 index 00000000..e9978519 --- /dev/null +++ b/tests/resources/imap-test/listext @@ -0,0 +1,95 @@ +state: auth +capabilities: LIST-EXTENDED + +# get the separator +ok list "" "" +* list () $sep "" + +ok create $mailbox${sep} +ok create $mailbox${sep}test1 +ok create $mailbox${sep}test2${sep} +ok create $mailbox${sep}test2${sep}test1 +ok create $mailbox${sep}test3 +ok create $mailbox${sep}test4${sep}test5 +ok create $mailbox${sep}test4${sep}test52 +ok create $mailbox${sep}test6 +ok create $mailbox${sep}test7${sep}test7 +ok create $mailbox${sep}test8${sep}test8 +ok create $mailbox${sep}test8${sep}test9 + +ok subscribe $mailbox${sep}test1 +ok subscribe $mailbox${sep}test2${sep}test1 +ok subscribe $mailbox${sep}test6 +ok subscribe $mailbox${sep}test7${sep}test7 +ok subscribe $mailbox${sep}test8${sep}test8 +ok subscribe $mailbox${sep}test8${sep}test9 + +ok delete $mailbox${sep}test6 + +# "" isn't a special case with LIST-EXTENDED +#ok list () "" "" +#! list () $sep "" +#ok list "" ("") +#! list () $sep "" +#ok list "" "" return () +#! list () $sep "" + +# test multiple patterns +ok list "" ($mailbox${sep}*2 $mailbox${sep}test3) +! list () $sep $mailbox${sep}test1 +* list () $sep $mailbox${sep}test2 +! list () $sep $mailbox${sep}test2${sep}test1 +* list () $sep $mailbox${sep}test3 +! list () $sep $mailbox${sep}test4 +! list () $sep $mailbox${sep}test4${sep}test5 +* list () $sep $mailbox${sep}test4${sep}test52 +! list () $sep $mailbox${sep}test6 + +# test errors +bad list (imaptest) "" "" +bad list (recursivematch) "" "" +bad list (recursivematch remote) "" "" +bad list "" "" return (imaptest) + +ok list (remote) "" % + +ok list (subscribed) "" $mailbox${sep}* +* list (\subscribed) $sep $mailbox${sep}test1 +* list (\subscribed) $sep $mailbox${sep}test2${sep}test1 +#* list ($!unordered $!ban=\noselect \subscribed \nonexistent) $sep $mailbox${sep}test6 +* list (\subscribed) $sep $mailbox${sep}test7${sep}test7 +* list (\subscribed) $sep $mailbox${sep}test8${sep}test9 + +ok list (subscribed recursivematch) "" $mailbox${sep}test2% return (children) +* list (\haschildren) $sep $mailbox${sep}test2 (childinfo (subscribed)) +! list (\subscribed) $sep $mailbox${sep}test2 +! list (\subscribed) $sep $mailbox${sep}test2${sep}test1 + +# don't test for \hasnochildren, because it may be \noinferiors instead +ok list "" $mailbox${sep}% return (children subscribed) +* list ($!unordered $!ban=\haschildren \subscribed) $sep $mailbox${sep}test1 +* list ($!unordered $!ban=\subscribed \haschildren) $sep $mailbox${sep}test2 +* list ($!unordered $!ban=\subscribed $!ban=\haschildren) $sep $mailbox${sep}test3 +* list ($!unordered $!ban=\subscribed \haschildren) $sep $mailbox${sep}test4 +* list ($!unordered $!ban=\subscribed \haschildren) $sep $mailbox${sep}test7 +* list ($!unordered $!ban=\subscribed \haschildren) $sep $mailbox${sep}test8 + +ok list (subscribed recursivematch) "" $mailbox*test7 +! list () $sep $mailbox${sep}test7 +* list (\subscribed) $sep $mailbox${sep}test7${sep}test7 + +ok list (subscribed recursivematch) "" $mailbox*test8 +* list ($!unordered $!ban=\subscribed) $sep $mailbox${sep}test8 (childinfo ("subscribed")) +* list (\subscribed) $sep $mailbox${sep}test8${sep}test8 + +ok list (subscribed recursivematch) "" ($mailbox${sep}test2) +* list () $sep $mailbox${sep}test2 (childinfo ("subscribed")) +! list () $sep $mailbox${sep}test2${sep}test1 + +ok list (subscribed recursivematch) "" ($mailbox${sep}test2 $mailbox${sep}test2${sep}test1) +* list () $sep $mailbox${sep}test2 (childinfo ("subscribed")) +* list (\subscribed) $sep $mailbox${sep}test2${sep}test1 + +ok list "" ($mailbox${sep}test7 $mailbox${sep}test7${sep}test7) +#This isn't really an error, although it's non-optimal: ! list (\nonexistent) $sep $mailbox${sep}test7 +* list () $sep $mailbox${sep}test7${sep}test7 diff --git a/tests/resources/imap-test/logout b/tests/resources/imap-test/logout new file mode 100644 index 00000000..3deecab3 --- /dev/null +++ b/tests/resources/imap-test/logout @@ -0,0 +1,14 @@ +state: nonauth +connections: 2 + +1 ok noop +! bye +2 ok noop +! bye + +1 ok logout +* bye + +2 ok noop +2 ok logout +* bye diff --git a/tests/resources/imap-test/move b/tests/resources/imap-test/move new file mode 100644 index 00000000..d3e79e0c --- /dev/null +++ b/tests/resources/imap-test/move @@ -0,0 +1,47 @@ +capabilities: MOVE +state: created + +# - assumes COPYUID is sent untagged before expunges. +# - assumes MOVE to mailbox itself changes message UID + +ok append +ok append +ok append +ok create ${mailbox}2 + +ok select ${mailbox}2 +* 0 exists +* ok [uidvalidity $uidvalidity_dest] +* ok [uidnext $uidnext_dest1] + +# MOVE: +ok select $mailbox + +ok fetch 1:* uid +* 1 fetch (uid $uid1) +* 2 fetch (uid $uid2) +* 3 fetch (uid $uid3) + +ok move 1 ${mailbox}2 +* ok [copyuid $uidvalidity_dest $uid1 $uidnext_dest1] +* 1 expunge + +# UID MOVE: +ok select ${mailbox}2 +* 1 exists +* ok [uidvalidity $uidvalidity_dest] +* ok [uidnext $uidnext_dest2] + +ok select $mailbox +* ok [uidvalidity $uidvalidity] +* ok [uidnext $uidnext1] + +ok uid move $uid2 ${mailbox}2 +* ok [copyuid $uidvalidity_dest $uid2 $uidnext_dest2] +* 1 expunge + +# MOVE to same mailbox: +#ok move 1 $mailbox +#* ok [copyuid $uidvalidity $uid3 $uidnext1] +#* 1 expunge +#* 1 exists diff --git a/tests/resources/imap-test/multiappend b/tests/resources/imap-test/multiappend new file mode 100644 index 00000000..0751a6dc --- /dev/null +++ b/tests/resources/imap-test/multiappend @@ -0,0 +1,30 @@ +capabilities: MULTIAPPEND +state: created + +ok append $mailbox {{{ +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: mail1 + +body1 +}}} {{{ +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: mail2 + +body2 +}}} + +ok select $mailbox + +ok fetch 1:* body.peek[header.fields (subject)] +* 1 fetch (body[header.fields (subject)] {{{ +Subject: mail1 + + +}}}) +* 2 fetch (body[header.fields (subject)] {{{ +Subject: mail2 + + +}}}) diff --git a/tests/resources/imap-test/mutf7 b/tests/resources/imap-test/mutf7 new file mode 100644 index 00000000..d20c1344 --- /dev/null +++ b/tests/resources/imap-test/mutf7 @@ -0,0 +1,12 @@ +state: auth + +# get the separator +ok list "" "" +* list () $sep $root + +ok create "$mailbox${sep}p&AOQA5A-" +ok list "" "$mailbox${sep}p&AOQA5A-" +* list () $sep "$mailbox${sep}p&AOQA5A-" + +ok status "$mailbox${sep}p&AOQA5A-" (messages) +* status "$mailbox${sep}p&AOQA5A-" (messages 0) diff --git a/tests/resources/imap-test/nil b/tests/resources/imap-test/nil new file mode 100644 index 00000000..178170a7 --- /dev/null +++ b/tests/resources/imap-test/nil @@ -0,0 +1,13 @@ +messages: 1 + +ok search subject NIL +* search 1 + +ok search body nil +* search 1 + +ok list "" NIL + +ok store 1 +flags NIL +ok fetch 1 flags +* 1 fetch (flags (NIL)) diff --git a/tests/resources/imap-test/nil.mbox b/tests/resources/imap-test/nil.mbox new file mode 100644 index 00000000..dfe1b9a5 --- /dev/null +++ b/tests/resources/imap-test/nil.mbox @@ -0,0 +1,5 @@ +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: NiL + +nil diff --git a/tests/resources/imap-test/notify b/tests/resources/imap-test/notify new file mode 100644 index 00000000..83d6940b --- /dev/null +++ b/tests/resources/imap-test/notify @@ -0,0 +1,20 @@ +capabilities: notify qresync + +ok create ${mailbox}2 +ok append ${mailbox}2 + +# +# Check that initial STATUS notifications are sent when needed +# + +ok notify set status (mailboxes ${mailbox}2 (MessageExpunge)) +* status ${mailbox}2 (messages 1) + +ok notify set status (mailboxes ${mailbox}2 (MessageNew)) +* status ${mailbox}2 (messages 1 uidnext $uidnext uidvalidity $uidvalidity) + +ok notify set status (mailboxes ${mailbox}2 (FlagChange)) +* status ${mailbox}2 (uidvalidity $uidvalidity highestmodseq $hmodseq) + +ok notify set (mailboxes ${mailbox}2 (MessageNew MessageExpunge FlagChange)) +! status ${mailbox}2 () diff --git a/tests/resources/imap-test/pipeline b/tests/resources/imap-test/pipeline new file mode 100644 index 00000000..057e68e4 --- /dev/null +++ b/tests/resources/imap-test/pipeline @@ -0,0 +1,23 @@ +state: auth + +tag1 create ${mailbox} +tag2 create ${mailbox}2 +tag3 create ${mailbox}3 +tag4 create ${mailbox}4 +tag1 ok +tag2 ok +tag3 ok +tag4 ok + +tag1 status ${mailbox} (messages) +tag2 status ${mailbox}2 (messages) +tag3 status ${mailbox}3 (messages) +tag4 status ${mailbox}4 (messages) +* status ${mailbox} (messages 0) +* status ${mailbox}2 (messages 0) +* status ${mailbox}3 (messages 0) +* status ${mailbox}4 (messages 0) +tag1 ok +tag2 ok +tag3 ok +tag4 ok diff --git a/tests/resources/imap-test/search-addresses b/tests/resources/imap-test/search-addresses new file mode 100644 index 00000000..5efc9405 --- /dev/null +++ b/tests/resources/imap-test/search-addresses @@ -0,0 +1,71 @@ +messages: all + +# full address searching +ok search from user-from@domain.org +* search 1 2 3 4 6 7 +ok search to user-to@domain.org +* search 1 2 3 4 +ok search cc user-cc@domain.org +* search 1 2 3 4 5 +ok search bcc user-bcc@domain.org +* search 1 2 3 4 + +# realname searching +ok search from realfrom +* search 2 4 6 7 +ok search to realto +* search 2 4 +ok search cc realcc +* search 2 4 +ok search bcc realbcc +* search 2 4 + +# existence searches +ok search header from "" +* search 1 2 3 4 5 6 7 +ok search header to "" +* search 1 2 3 4 6 7 +ok search header cc "" +* search 1 2 3 4 5 +ok search header bcc "" +* search 1 2 3 4 + +# substring address searches +#ok search from ser-fro +#* search 1 2 3 4 5 6 7 +#ok search to ser-t +#* search 1 2 3 4 +#ok search cc ser-c +#* search 1 2 3 4 5 +#ok search bcc ser-bc +#* search 1 2 3 4 + +# substring realname searches +#ok search from ealfro +#* search 2 4 6 7 +#ok search to ealt +#* search 2 4 +#ok search cc ealc +#* search 2 4 +#ok search bcc ealbc +#* search 2 4 + +# multiple addresses +ok search from user-from1 +* search 5 +ok search from user-from2 +* search 5 + +# groups +ok search to groupname +* search 6 7 +ok search to groupname2 +* search 6 +ok search to groupuser1 +* search 6 +ok search to groupuser2 +* search 6 +ok search to groupuser3 +* search 6 +ok search to groupuser4 +* search diff --git a/tests/resources/imap-test/search-addresses.mbox b/tests/resources/imap-test/search-addresses.mbox new file mode 100644 index 00000000..416eae12 --- /dev/null +++ b/tests/resources/imap-test/search-addresses.mbox @@ -0,0 +1,50 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user-from@domain.org +To: user-to@domain.org +Cc: user-cc@domain.org +Bcc: user-bcc@domain.org + +body + +From user@domain Fri Feb 22 17:06:23 2008 +From: RealFrom +To: RealTo +Cc: RealCc +Bcc: RealBcc + +body2 + +From user@domain Fri Feb 22 17:06:23 2008 +From: +To: +Cc: +Bcc: + +body3 + +From user@domain Fri Feb 22 17:06:23 2008 +From: user-from@domain.org (RealFrom) +To: user-to@domain.org (RealTo) +Cc: user-cc@domain.org (RealCc) +Bcc: user-bcc@domain.org (RealBcc) + +body4 + +From user@domain Fri Feb 22 17:06:23 2008 +From: user-from1@domain.org, user-from2@domain.org +Cc: user-cc@domain.org + +body5 + +From user@domain Fri Feb 22 17:06:23 2008 +From: RealFrom +To: groupname: groupuser1@domain.org, groupuser2@domain.org;, + groupname2: groupuser3@domain.org; + +body6 + +From user@domain Fri Feb 22 17:06:23 2008 +From: RealFrom +To: groupname:; + +body7 diff --git a/tests/resources/imap-test/search-body b/tests/resources/imap-test/search-body new file mode 100644 index 00000000..789bf291 --- /dev/null +++ b/tests/resources/imap-test/search-body @@ -0,0 +1,31 @@ +messages: all + +# search full words first +ok search text asdfghjkl +* search 1 2 +ok search text zxcvbnm +* search 1 3 4 +ok search text qwertyuiop +* search 2 4 + +ok search body asdfghjkl +* search 1 2 +ok search body zxcvbnm +* search 1 3 4 +ok search body qwertyuiop +* search 4 + +# search substrings +#ok search text sdfghjk +#* search 1 2 +#ok search text xcvbn +#* search 1 3 4 +#ok search text wertyuio +#* search 2 3 4 + +#ok search body sdfghjk +#* search 1 2 +#ok search body xcvbn +#* search 1 3 4 +#ok search body wertyuio +#* search 4 diff --git a/tests/resources/imap-test/search-body.mbox b/tests/resources/imap-test/search-body.mbox new file mode 100644 index 00000000..91ab4107 --- /dev/null +++ b/tests/resources/imap-test/search-body.mbox @@ -0,0 +1,21 @@ +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +asdfghjkl zxcvbnm + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Subject: qwertyuiop + +asdfghjkl + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 +X-Header: qwertyuiop + +zxcvbnm + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +zxcvbnm qwertyuiop diff --git a/tests/resources/imap-test/search-context-update b/tests/resources/imap-test/search-context-update new file mode 100644 index 00000000..f4bdaaa8 --- /dev/null +++ b/tests/resources/imap-test/search-context-update @@ -0,0 +1,35 @@ +capabilities: CONTEXT=SEARCH +state: created + +ok append +ok append +ok append +ok append +ok append + +ok select $mailbox + +ok search return (update) body body seen +* esearch (tag $searchtag) + +ok store 1,2,4 +flags \seen +* esearch (tag $searchtag) addto ($pos 1:2,4) + +ok store 1,2 -flags \seen +* esearch (tag $searchtag) removefrom ($pos2 1:2) + +ok store 3:5 +flags \deleted +ok expunge +* esearch (tag $searchtag) removefrom ($pos3 $4) +* $3 expunge +* $4 expunge +* $5 expunge + +ok append $mailbox (\seen) +* 3 exists +* esearch (tag $searchtag) addto ($pos4 3) + +ok cancelupdate "$searchtag" + +ok store 1 +flags \seen +! esearch (tag $searchtag) addto ($pos5 1) diff --git a/tests/resources/imap-test/search-context-update2 b/tests/resources/imap-test/search-context-update2 new file mode 100644 index 00000000..76eb2b65 --- /dev/null +++ b/tests/resources/imap-test/search-context-update2 @@ -0,0 +1,21 @@ +capabilities: CONTEXT=SEARCH +messages: 5 + +ok store 1,2 +flags \seen + +ok search return (update all) or seen subject s22 +* esearch (tag $searchtag) all 1:2 + +ok store 3:5 +flags \seen +* esearch (tag $searchtag) addto ($pos 3:5) + +ok store 1:5 -flags \seen +* esearch (tag $searchtag) removefrom ($pos2 1,3:5) + +ok store 3,4 +flags \seen +* esearch (tag $searchtag) addto ($pos3 3:4) + +ok store 3 +flags \deleted +ok expunge +* 3 expunge +* esearch (tag $searchtag) removefrom ($pos4 3) diff --git a/tests/resources/imap-test/search-context-update3 b/tests/resources/imap-test/search-context-update3 new file mode 100644 index 00000000..f427da29 --- /dev/null +++ b/tests/resources/imap-test/search-context-update3 @@ -0,0 +1,31 @@ +capabilities: CONTEXT=SEARCH +messages: 6 + +ok store 1 +flags \deleted +ok expunge + +ok fetch 1:5 uid +* 1 fetch (uid $uid1) +* 2 fetch (uid $uid2) +* 3 fetch (uid $uid3) +* 4 fetch (uid $uid4) +* 5 fetch (uid $uid5) + +ok store 1,2 +flags \seen + +ok uid search return (update all) or seen subject s33 +* esearch (tag $searchtag) uid all $uid1:$uid2 + +ok store 3:5 +flags \seen +* esearch (tag $searchtag) uid addto ($pos $uid3:$uid5) + +ok store 1:5 -flags \seen +* esearch (tag $searchtag) uid removefrom ($pos2 $uid1,$uid3:$uid5) + +ok uid store $uid3,$uid4 +flags \seen +* esearch (tag $searchtag) uid addto ($pos3 $uid3:$uid4) + +ok store 3 +flags \deleted +ok expunge +* 3 expunge +* esearch (tag $searchtag) uid removefrom ($pos4 $uid3) diff --git a/tests/resources/imap-test/search-date b/tests/resources/imap-test/search-date new file mode 100644 index 00000000..6c51895f --- /dev/null +++ b/tests/resources/imap-test/search-date @@ -0,0 +1,73 @@ +messages: all + +# 1) Timezone changes from EET +0200 -> EEST +0300 +# 1a) SENTBEFORE +ok search sentbefore 24-mar-2007 +* search +ok search sentbefore 25-mar-2007 +* search 1 +ok search sentbefore 26-mar-2007 +* search 1 2 3 4 5 6 +ok search sentbefore 27-mar-2007 +* search 1 2 3 4 5 6 7 + +# 1b) SENTON +ok search senton 23-mar-2007 +* search +ok search senton 24-mar-2007 +* search 1 +ok search senton 25-mar-2007 +* search 2 3 4 5 6 +ok search senton 26-mar-2007 +* search 7 + +# 1c) SENTSINCE +ok search 1:7 sentsince 24-mar-2007 +* search 1 2 3 4 5 6 7 +ok search 1:7 sentsince 25-mar-2007 +* search 2 3 4 5 6 7 +ok search 1:7 sentsince 26-mar-2007 +* search 7 +ok search 1:7 sentsince 27-mar-2007 +* search + +# 2) Timezone changes from EEST +0300 -> EET +0200 +# 2a) SENTBEFORE +ok search 8:* sentbefore 27-oct-2007 +* search +ok search 8:* sentbefore 28-oct-2007 +* search 8 +ok search 8:* sentbefore 29-oct-2007 +* search 8 9 10 11 12 13 14 15 +ok search 8:* sentbefore 30-oct-2007 +* search 8 9 10 11 12 13 14 15 16 + +# 2b) SENTON +ok search 8:* senton 26-oct-2007 +* search +ok search 8:* senton 27-oct-2007 +* search 8 +ok search 8:* senton 28-oct-2007 +* search 9 10 11 12 13 14 15 +ok search 8:* senton 29-oct-2007 +* search 16 + +# 2c) SENTSINCE +ok search 8:* sentsince 27-oct-2007 +* search 8 9 10 11 12 13 14 15 16 +ok search 8:* sentsince 28-oct-2007 +* search 9 10 11 12 13 14 15 16 +ok search 8:* sentsince 29-oct-2007 +* search 16 +ok search 8:* sentsince 30-oct-2007 +* search + +# 3) Try a couple of NOTs +ok search 1:7 not sentbefore 26-mar-2007 +* search 7 +ok search 1:7 not senton 25-mar-2007 +* search 1 7 +ok search 8:* not sentsince 28-oct-2007 +* search 8 +ok search 8:* not senton 28-oct-2007 +* search 8 16 diff --git a/tests/resources/imap-test/search-date.mbox b/tests/resources/imap-test/search-date.mbox new file mode 100644 index 00000000..484db1f5 --- /dev/null +++ b/tests/resources/imap-test/search-date.mbox @@ -0,0 +1,80 @@ +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 25 Mar 2007 00:00:00 +0200 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 25 Mar 2007 01:00:00 +0200 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 25 Mar 2007 02:00:00 +0200 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 25 Mar 2007 04:00:00 +0300 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 25 Mar 2007 23:00:00 +0300 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Mon, 26 Mar 2007 00:00:00 +0300 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sat, 27 Oct 2007 03:00:00 +0300 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 28 Oct 2007 00:00:00 +0300 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 28 Oct 2007 01:00:00 +0300 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 28 Oct 2007 02:00:00 +0300 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 28 Oct 2007 03:00:00 +0300 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 28 Oct 2007 03:00:00 +0200 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 28 Oct 2007 04:00:00 +0200 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Sun, 28 Oct 2007 23:00:00 +0200 + +body + +From user@domain Sat Mar 24 23:00:00 2007 +0200 +Date: Mon, 29 Oct 2007 00:00:00 +0200 + +body + diff --git a/tests/resources/imap-test/search-flags b/tests/resources/imap-test/search-flags new file mode 100644 index 00000000..557df91f --- /dev/null +++ b/tests/resources/imap-test/search-flags @@ -0,0 +1,95 @@ +messages: 5 + +ok store 1 flags ($$hello) +ok store 2 flags (\seen \flagged) +ok store 3 flags (\answered $$hello) +ok store 4 flags (\flagged \draft) +ok store 5 flags (\deleted \answered) + +ok search answered +* search 3 5 +ok search unanswered +* search 1 2 4 + +ok search deleted +* search 5 +ok search undeleted +* search 1 2 3 4 + +ok search draft +* search 4 +ok search undraft +* search 1 2 3 5 + +ok search flagged +* search 2 4 +ok search unflagged +* search 1 3 5 + +ok search seen +* search 2 +ok search unseen +* search 1 3 4 5 + +ok search keyword $$hello +* search 1 3 +ok search unkeyword $$hello +* search 2 4 5 + +#ok search new +#* search 1 3 4 5 + +#ok search old +#* search + +#ok search recent +#* search 1 2 3 4 5 + +ok store 1:* flags (\seen) +ok store 2 +flags (\flagged) +ok search seen flagged +* search 2 +ok search seen not flagged +* search 1 3 4 5 +ok search not seen flagged +* search +ok search not seen not flagged +* search + +ok store 1:* flags (\deleted) +ok store 2 +flags (\flagged) +ok search deleted flagged +* search 2 +ok search deleted not flagged +* search 1 3 4 5 +ok search not deleted flagged +* search +ok search not deleted not flagged +* search + +ok store 1:* flags (\seen \deleted) +ok store 2 +flags (\flagged) +ok search seen flagged +* search 2 +ok search seen not flagged +* search 1 3 4 5 +ok search not seen flagged +* search +ok search not seen not flagged +* search +ok search seen deleted flagged +* search 2 +ok search seen deleted not flagged +* search 1 3 4 5 +ok search not seen deleted flagged +* search +ok search not seen deleted not flagged +* search +ok search seen not deleted flagged +* search +ok search seen not deleted not flagged +* search +ok search not seen not deleted flagged +* search +ok search not seen not deleted not flagged +* search diff --git a/tests/resources/imap-test/search-header b/tests/resources/imap-test/search-header new file mode 100644 index 00000000..36689946 --- /dev/null +++ b/tests/resources/imap-test/search-header @@ -0,0 +1,37 @@ +messages: all + +# just check that this returns ok. it's not really specified in RFC, so +# don't verify the result. +ok search subject "" + +# subject +ok search subject hello +* search 1 +ok search subject beautiful +* search 1 +ok search subject world +* search 1 +ok search subject "hello beautiful" +* search 1 +ok search subject "hello beautiful world" +* search 1 +#ok search subject "eautiful worl" +#* search 1 + +# header +ok search header subject "" +* search 1 +ok search not header subject "" +* search 2 +#ok search header x-extra "" +#* search 2 +#ok search not header x-extra "" +#* search 1 +#ok search header x-extra hello +#* search 2 +#ok search header x-extra "hello beautiful" +#* search 2 +#ok search header x-extra "eautiful head" +#* search 2 +#ok search header x-extra "another" +#* search 2 diff --git a/tests/resources/imap-test/search-header.mbox b/tests/resources/imap-test/search-header.mbox new file mode 100644 index 00000000..943f9937 --- /dev/null +++ b/tests/resources/imap-test/search-header.mbox @@ -0,0 +1,10 @@ +From user@domain Fri Feb 22 17:06:23 2008 +Subject: hello beautiful world + +body + +From user@domain Fri Feb 22 17:06:23 2008 +X-Extra: hello beautiful header +X-Extra: another one + +body diff --git a/tests/resources/imap-test/search-partial b/tests/resources/imap-test/search-partial new file mode 100644 index 00000000..7be35de4 --- /dev/null +++ b/tests/resources/imap-test/search-partial @@ -0,0 +1,54 @@ +capabilities: CONTEXT=SEARCH +messages: all + +ok search return (partial 1:1) all +* esearch (tag $tag) partial (1:1 1) +ok search return (partial 2:4) all +* esearch (tag $tag) partial (2:4 2:4) +ok search return (partial 4:2) all +* esearch (tag $tag) partial (2:4 2:4) +ok search return (partial 1:6) all +* esearch (tag $tag) partial (1:6 1:5) +ok search return (partial 6:6) all +* esearch (tag $tag) partial (6:6 nil) + +ok search return (partial 1:3) 1:2,4:5 +* esearch (tag $tag) partial (1:3 1:2,4) +ok search return (partial 2:3) 1:2,4:5 +* esearch (tag $tag) partial (2:3 2,4) +ok search return (partial 2:4) 1:2,4:5 +* esearch (tag $tag) partial (2:4 2,4:5) +ok search return (partial 2:10) 1:2,4:5 +* esearch (tag $tag) partial (2:10 2,4:5) + +# UID partials + +ok fetch 1 uid +* 1 fetch (uid $uid1) +ok fetch 2 uid +* 2 fetch (uid $uid2) +ok fetch 3 uid +* 3 fetch (uid $uid3) +ok fetch 5 uid +* 5 fetch (uid $uid5) + +ok uid search return (partial 1:1) all +* esearch (tag $tag) uid partial (1:1 $uid1) + +ok uid search return (partial 2:2) all +* esearch (tag $tag) uid partial (2:2 $uid2) + +ok store 2 +flags \deleted +ok expunge + +ok uid search return (partial 2:2) all +* esearch (tag $tag) uid partial (2:2 $uid3) + +ok uid search return (partial 5:10) all +* esearch (tag $tag) uid partial (5:10 nil) + +# broken results + +bad search return (partial 1) all +bad search return (partial 1:*) all +bad search return (partial *:1) all diff --git a/tests/resources/imap-test/search-partial.mbox b/tests/resources/imap-test/search-partial.mbox new file mode 100644 index 00000000..132ea16c --- /dev/null +++ b/tests/resources/imap-test/search-partial.mbox @@ -0,0 +1,19 @@ +From user@domain Fri Feb 22 17:06:21 2008 + +1 + +From user@domain Fri Feb 22 17:06:22 2008 + +2 + +From user@domain Fri Feb 22 17:06:23 2008 + +3 + +From user@domain Fri Feb 22 17:06:23 2008 + +4 + +From user@domain Fri Feb 22 17:06:25 2008 + +5 diff --git a/tests/resources/imap-test/search-sets b/tests/resources/imap-test/search-sets new file mode 100644 index 00000000..cd0009e6 --- /dev/null +++ b/tests/resources/imap-test/search-sets @@ -0,0 +1,62 @@ +messages: 6 + +ok uid search all +* search $uid1 $uid2 $uid3 $uid4 $uid5 $uid6 +ok status $mailbox (uidnext) +* status $mailbox (uidnext $uidnext) + +# break seq=uid mapping +ok store 2 +flags \deleted +ok expunge +* 2 expunge + +ok search all +* search 1 2 3 4 5 +ok uid search all +* search $uid1 $uid3 $uid4 $uid5 $uid6 + +ok search 1:3,5 +* search 1 2 3 5 +ok search 4:2 +* search 2 3 4 +ok search uid $uid1:$uid3,$uid5 +* search 1 2 4 +ok search uid $uid4:$uid2 +* search 2 3 + +ok search 1:3 not uid $uid3 +* search 1 3 +ok search not 2,4 +* search 1 3 5 +ok search or 1 uid $uid3 +* search 1 2 + +ok search * +* search 5 +ok search uid * +* search 5 +ok search uid $uidnext:* +* search 5 +ok search *:3 +* search 3 4 5 +ok search 1,4,* +* search 1 4 5 + +# These are in a bit of a grey area. Most servers allow them, but it's not +# explicitly defined in the RFC that they're legal: +#ok search 6:* +#* search 5 +#ok search *:6 +#* search 5 + +ok search (3) uid $uid4 +* search 3 +ok search (uid $uid4) 3 +* search 3 +ok search 3 (uid $uid4) +* search 3 + +ok uid search uid 1:4294967295 +* search $uid1 $uid3 $uid4 $uid5 $uid6 +ok uid search uid $uidnext:4294967295 +* search diff --git a/tests/resources/imap-test/search-size b/tests/resources/imap-test/search-size new file mode 100644 index 00000000..9f94f3bd --- /dev/null +++ b/tests/resources/imap-test/search-size @@ -0,0 +1,22 @@ +messages: all + +# get the middle size. don't trust precalculated values in case server +# modifies the message while APPENDing it. Messages 3 and 4 have different +# RFC822.SIZE, but with servers that store linefeeds as LFs they have the +# same file size. This test catches if they search using file size. +ok fetch 1:4 rfc822.size +* 3 fetch (rfc822.size $size) + +ok search smaller $size +* search 1 2 +ok search larger $size +* search 4 +ok search not smaller $size +* search 3 4 +ok search not larger $size +* search 1 2 3 + +ok search not smaller $size not larger $size +* search 3 +ok search or smaller $size larger $size +* search 1 2 4 diff --git a/tests/resources/imap-test/search-size.mbox b/tests/resources/imap-test/search-size.mbox new file mode 100644 index 00000000..c1457fa8 --- /dev/null +++ b/tests/resources/imap-test/search-size.mbox @@ -0,0 +1,21 @@ +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +1 + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +22 + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +333 + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +4 +4 + diff --git a/tests/resources/imap-test/select b/tests/resources/imap-test/select new file mode 100644 index 00000000..0f130800 --- /dev/null +++ b/tests/resources/imap-test/select @@ -0,0 +1,33 @@ +state: created + +ok append +ok append + +# first open read-only so recent flags don't get lost +examine $mailbox +* 2 exists +#* 2 recent +#* ok [unseen 1] +* ok [uidvalidity $uidvalidity] +* ok [uidnext $uidnext] +ok [read-only] + +ok close + +# check that STATUS replies with the same values +ok status $mailbox (messages uidnext uidvalidity unseen) +* status $mailbox (messages 2 uidnext $uidnext uidvalidity $uidvalidity unseen 2) + +# then try read-write +select $mailbox +* 2 exists +#* 2 recent +#* ok [unseen 1] +* ok [uidvalidity $uidvalidity] +* ok [uidnext $uidnext] +ok [read-write] + +ok close + +#ok status $mailbox (recent) +#* status $mailbox (recent 0) diff --git a/tests/resources/imap-test/select.mbox b/tests/resources/imap-test/select.mbox new file mode 100644 index 00000000..0bb3c963 --- /dev/null +++ b/tests/resources/imap-test/select.mbox @@ -0,0 +1,10 @@ +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body + +From user@domain Fri Feb 22 17:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +body2 + diff --git a/tests/resources/imap-test/sort-addresses b/tests/resources/imap-test/sort-addresses new file mode 100644 index 00000000..ba7cb2e8 --- /dev/null +++ b/tests/resources/imap-test/sort-addresses @@ -0,0 +1,23 @@ +capabilities: SORT +messages: all + +ok sort (from) us-ascii all +* sort 3 2 1 +ok sort (to) us-ascii all +* sort 3 1 2 +ok sort (cc) us-ascii all +* sort 3 1 2 + +ok sort (reverse from) us-ascii all +* sort 1 2 3 +ok sort (reverse to) us-ascii all +* sort 1 2 3 +ok sort (reverse cc) us-ascii all +* sort 1 2 3 + +ok sort (from reverse arrival) us-ascii all +* sort 3 2 1 +ok sort (to reverse arrival) us-ascii all +* sort 3 2 1 +ok sort (cc reverse arrival) us-ascii all +* sort 3 2 1 diff --git a/tests/resources/imap-test/sort-addresses.mbox b/tests/resources/imap-test/sort-addresses.mbox new file mode 100644 index 00000000..52b1405f --- /dev/null +++ b/tests/resources/imap-test/sort-addresses.mbox @@ -0,0 +1,21 @@ +From user@domain Fri Feb 22 00:06:23 2008 +From: user2@domain2.org +To: foo bar +Cc: user2@domain2.org (foo bar) + +1 + +From user@domain Fri Feb 22 01:06:23 2008 +From: user2@domain1.org +To: user2@domain1.org +Cc: user2: blah@domain1.org; + +2 + +From user@domain Fri Feb 22 02:06:23 2008 +From: user1@domain1.org +To: user1@domain1.org +Cc: user1@domain1.org + +3 + diff --git a/tests/resources/imap-test/sort-arrival b/tests/resources/imap-test/sort-arrival new file mode 100644 index 00000000..9cc5c73a --- /dev/null +++ b/tests/resources/imap-test/sort-arrival @@ -0,0 +1,11 @@ +capabilities: SORT +messages: all + +ok sort (arrival) us-ascii all +* sort 1 3 2 4 5 + +ok sort (reverse arrival) us-ascii all +* sort 5 4 2 3 1 + +ok sort (arrival reverse size) us-ascii all +* sort 1 3 5 4 2 diff --git a/tests/resources/imap-test/sort-arrival.mbox b/tests/resources/imap-test/sort-arrival.mbox new file mode 100644 index 00000000..a603a074 --- /dev/null +++ b/tests/resources/imap-test/sort-arrival.mbox @@ -0,0 +1,20 @@ +From user@domain Fri Feb 22 00:00:00 2008 + +1 + +From user@domain Fri Feb 22 02:00:00 2008 + +2 + +From user@domain Fri Feb 22 01:00:00 2008 + +33 + +From user@domain Fri Feb 22 02:00:00 2008 + +44 + +From user@domain Fri Feb 22 02:00:00 2008 + +5555 + diff --git a/tests/resources/imap-test/sort-date b/tests/resources/imap-test/sort-date new file mode 100644 index 00000000..a4b65592 --- /dev/null +++ b/tests/resources/imap-test/sort-date @@ -0,0 +1,11 @@ +capabilities: SORT +messages: all + +ok sort (date) us-ascii all +* sort 1 3 7 5 2 4 6 + +ok sort (reverse date) us-ascii all +* sort 2 4 6 5 3 7 1 + +ok sort (date reverse size) us-ascii all +* sort 1 7 3 5 6 4 2 diff --git a/tests/resources/imap-test/sort-date.mbox b/tests/resources/imap-test/sort-date.mbox new file mode 100644 index 00000000..750f0bae --- /dev/null +++ b/tests/resources/imap-test/sort-date.mbox @@ -0,0 +1,35 @@ +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Date: Fri, 22 Feb 2008 00:00:00 +0200 + +1 + +From user@domain Fri Feb 22 22:00:00 2008 +0200 +Date: Fri, 22 Feb 2008 02:00:00 +0200 + +2 + +From user@domain Fri Feb 22 21:00:00 2008 +0200 +Date: Fri, 22 Feb 2008 01:00:00 +0200 + +33 + +From user@domain Fri Feb 22 20:00:00 2008 +0200 +Date: Fri, 22 Feb 2008 02:00:00 +0200 + +44 + +From user@domain Fri Feb 22 01:30:23 2008 +0200 +Subject: foo + +55555 + +From user@domain Fri Feb 22 18:00:00 2008 +0200 +Date: Fri, 22 Feb 2008 02:00:00 +0200 + +6666 + +From user@domain Fri Feb 22 01:00:00 2008 +0200 +Subject: foo bar foo bar foo bar fooo + +777 + diff --git a/tests/resources/imap-test/sort-display-from b/tests/resources/imap-test/sort-display-from new file mode 100644 index 00000000..8a7e8271 --- /dev/null +++ b/tests/resources/imap-test/sort-display-from @@ -0,0 +1,8 @@ +capabilities: SORT=DISPLAY +messages: all + +ok sort (displayfrom) us-ascii all +* sort 2 3 4 1 5 + +ok sort (reverse displayfrom) us-ascii all +* sort 5 1 4 3 2 diff --git a/tests/resources/imap-test/sort-display-from.mbox b/tests/resources/imap-test/sort-display-from.mbox new file mode 100644 index 00000000..393c655e --- /dev/null +++ b/tests/resources/imap-test/sort-display-from.mbox @@ -0,0 +1,24 @@ +From user@domain Fri Feb 22 23:00:00 2008 +0200 +From: foo bar + +1 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +From: b@c.d + +2 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +From: d + +3 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +From: =?iso-8859-1?q?foo_aar?= + +4 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +From: =?iso-8859-1?q?foo_car?= + +5 diff --git a/tests/resources/imap-test/sort-display-to b/tests/resources/imap-test/sort-display-to new file mode 100644 index 00000000..7722f460 --- /dev/null +++ b/tests/resources/imap-test/sort-display-to @@ -0,0 +1,8 @@ +capabilities: SORT=DISPLAY +messages: all + +ok sort (displayto) us-ascii all +* sort 6 2 3 8 4 1 5 7 + +ok sort (reverse displayto) us-ascii all +* sort 7 5 1 4 8 3 2 6 diff --git a/tests/resources/imap-test/sort-display-to.mbox b/tests/resources/imap-test/sort-display-to.mbox new file mode 100644 index 00000000..1b760a23 --- /dev/null +++ b/tests/resources/imap-test/sort-display-to.mbox @@ -0,0 +1,39 @@ +From user@domain Fri Feb 22 23:00:00 2008 +0200 +To: foo bar + +1 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +To: b@c.d + +2 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +To: d + +3 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +To: =?iso-8859-1?q?foo_aar?= + +4 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +To: =?iso-8859-1?q?foo_car?= + +5 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +To: a: xx@xx.org, yy@yy.org; + +6 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +To: z: aa@aa.org, bb@bb.org; + +7 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +To: empty:; + +8 diff --git a/tests/resources/imap-test/sort-partial b/tests/resources/imap-test/sort-partial new file mode 100644 index 00000000..c07d0f28 --- /dev/null +++ b/tests/resources/imap-test/sort-partial @@ -0,0 +1,19 @@ +capabilities: CONTEXT=SORT +messages: all + +ok sort return (partial 1:1) (arrival) us-ascii all +* esearch (tag $tag) partial (1:1 1) +ok sort return (partial 2:4) (arrival) us-ascii all +* esearch (tag $tag) partial (2:4 2:4) +ok sort return (partial 4:2) (arrival) us-ascii all +* esearch (tag $tag) partial (2:4 2:4) +ok sort return (partial 2:4) (reverse arrival) us-ascii all +* esearch (tag $tag) partial (2:4 3:4,2) +ok sort return (partial 1:6) (arrival) us-ascii all +* esearch (tag $tag) partial (1:6 1:5) +ok sort return (partial 1:6) (reverse arrival) us-ascii all +* esearch (tag $tag) partial (1:6 5,3:4,2,1) + +bad sort return (partial 1) (arrival) us-ascii all +bad sort return (partial 1:*) (arrival) us-ascii all +bad sort return (partial *:1) (arrival) us-ascii all diff --git a/tests/resources/imap-test/sort-partial.mbox b/tests/resources/imap-test/sort-partial.mbox new file mode 100644 index 00000000..132ea16c --- /dev/null +++ b/tests/resources/imap-test/sort-partial.mbox @@ -0,0 +1,19 @@ +From user@domain Fri Feb 22 17:06:21 2008 + +1 + +From user@domain Fri Feb 22 17:06:22 2008 + +2 + +From user@domain Fri Feb 22 17:06:23 2008 + +3 + +From user@domain Fri Feb 22 17:06:23 2008 + +4 + +From user@domain Fri Feb 22 17:06:25 2008 + +5 diff --git a/tests/resources/imap-test/sort-size b/tests/resources/imap-test/sort-size new file mode 100644 index 00000000..a7dcc47e --- /dev/null +++ b/tests/resources/imap-test/sort-size @@ -0,0 +1,11 @@ +capabilities: SORT +messages: all + +ok sort (size) us-ascii all +* sort 1 8 2 6 3 5 4 7 + +ok sort (reverse size) us-ascii all +* sort 7 4 5 3 6 2 8 1 + +ok sort (size reverse arrival) us-ascii all +* sort 8 1 6 2 5 3 7 4 diff --git a/tests/resources/imap-test/sort-size.mbox b/tests/resources/imap-test/sort-size.mbox new file mode 100644 index 00000000..1d9ec5a3 --- /dev/null +++ b/tests/resources/imap-test/sort-size.mbox @@ -0,0 +1,41 @@ +From user@domain Fri Feb 22 00:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +1 + +From user@domain Fri Feb 22 01:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +22 + +From user@domain Fri Feb 22 02:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +333 + +From user@domain Fri Feb 22 03:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +4 +4 + +From user@domain Fri Feb 22 04:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +555 + +From user@domain Fri Feb 22 05:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +66 + +From user@domain Fri Feb 22 06:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +7777 + +From user@domain Fri Feb 22 07:06:23 2008 +Date: Sat, 24 Mar 2007 23:00:00 +0200 + +8 + diff --git a/tests/resources/imap-test/sort-subject b/tests/resources/imap-test/sort-subject new file mode 100644 index 00000000..07155ead --- /dev/null +++ b/tests/resources/imap-test/sort-subject @@ -0,0 +1,11 @@ +capabilities: SORT +messages: all + +ok sort (subject) us-ascii all +* sort 9 10 1 14 3 5 11 6 15 2 7 12 13 8 4 + +ok sort (reverse subject) us-ascii all +* sort 4 8 2 7 12 13 15 6 3 5 11 14 1 9 10 + +ok sort (subject reverse size) us-ascii all +* sort 10 9 1 14 11 5 3 6 15 13 12 7 2 8 4 diff --git a/tests/resources/imap-test/sort-subject.mbox b/tests/resources/imap-test/sort-subject.mbox new file mode 100644 index 00000000..79853fec --- /dev/null +++ b/tests/resources/imap-test/sort-subject.mbox @@ -0,0 +1,81 @@ +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: a + +1 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: C + +2 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: b + +3 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: _ + +4 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: [foo] Fwd: [bar] Re: fw: b (fWd) (fwd) + +55 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: b (a) + +66 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: Re: [FWD: c] + +77 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: [xyz] + +88 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 + +9 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: =?iso-8859-1?q?_?= + +10 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: Re: =?utf-8?q?b?= + +11 +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: =?iso-8859-1?q?RE:_C?= + +12 +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: =?us-ascii?b?UmU6IGM=?= + +13 + +The subject is 'Re: c'. +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: Ad: Re: Ad: Re: Ad: x + +14 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: re: [fwd: [fwd: re: [fwd: babylon]]] + +15 diff --git a/tests/resources/imap-test/store b/tests/resources/imap-test/store new file mode 100644 index 00000000..96d8bef4 --- /dev/null +++ b/tests/resources/imap-test/store @@ -0,0 +1,33 @@ +connections: 2 +messages: 4 + +# simple tests +1 ok store 1:3 flags (\seen) +* 1 fetch (flags (\seen)) +* 2 fetch (flags (\seen)) +* 3 fetch (flags (\seen)) + +2 ok check +* 1 fetch (flags (\seen)) +* 2 fetch (flags (\seen)) +* 3 fetch (flags (\seen)) + +1 ok store 4 -flags \seen +1 ok store 3 flags (\draft) +* 3 fetch (flags (\draft)) + +# keywords +1 ok store 2,4 +flags ($$hello $$world) +* 2 fetch (flags (\seen $$hello $$world)) +* 4 fetch (flags ($$hello $$world)) + +# check that two sessions don't overwrite each others' changes +1 ok store 1 +flags (\answered) +2 ok store 1 -flags (\seen) +1 ok check +2 ok check + +1 ok fetch 1 flags +* 1 fetch (flags (\answered)) +2 ok fetch 1 flags +* 1 fetch (flags (\answered)) diff --git a/tests/resources/imap-test/subscribe b/tests/resources/imap-test/subscribe new file mode 100644 index 00000000..6740bce1 --- /dev/null +++ b/tests/resources/imap-test/subscribe @@ -0,0 +1,49 @@ +connections: 2 +state: auth + +# get the separator +1 ok list "" "" +* list () $sep $root + +1 "" unsubscribe $mailbox${sep}test +1 "" unsubscribe $mailbox${sep}test2 +1 "" unsubscribe $mailbox${sep}test2${sep}test +1 "" unsubscribe $mailbox${sep}test3${sep}test3 + +1 ok create $mailbox${sep} +1 ok create $mailbox${sep}test +1 ok create $mailbox${sep}test2${sep} +1 ok create $mailbox${sep}test3${sep} +1 ok create $mailbox${sep}test2${sep}test +1 ok create $mailbox${sep}test3${sep}test3 +# create the test2 mailbox only if server supports inferior mailboxes +1 "" create $mailbox${sep}test2 + +1 ok subscribe $mailbox${sep}test +1 ok subscribe $mailbox${sep}test2${sep}test +1 ok subscribe $mailbox${sep}test3${sep}test3 + +2 ok lsub "" $mailbox${sep}% +* lsub () $sep $mailbox${sep}test +#* lsub (\noselect) $sep $mailbox${sep}test2 +#* lsub (\noselect) $sep $mailbox${sep}test3 +! lsub (\noselect) $sep $mailbox${sep}test2${sep}test +! lsub (\noselect) $sep $mailbox${sep}test3${sep}test3 + +2 ok lsub "" *test +* lsub () $sep $mailbox${sep}test +* lsub () $sep $mailbox${sep}test2${sep}test +! lsub () $sep $mailbox${sep}test3${sep}test3 + +1 ok unsubscribe $mailbox${sep}test +2 ok lsub "" $mailbox${sep}% +! lsub () $sep $mailbox${sep}test + +1 ok unsubscribe $mailbox${sep}test2${sep}test +2 ok lsub "" $mailbox${sep}* +! lsub () $sep $mailbox${sep}test +* lsub () $sep $mailbox${sep}test3${sep}test3 + +1 ok unsubscribe $mailbox${sep}test3${sep}test3 +2 ok lsub "" $mailbox${sep}% +! lsub () $sep $mailbox${sep}test3 diff --git a/tests/resources/imap-test/thread b/tests/resources/imap-test/thread new file mode 100644 index 00000000..49654a59 --- /dev/null +++ b/tests/resources/imap-test/thread @@ -0,0 +1,11 @@ +capabilities: THREAD=REFERENCES +messages: all + +ok thread references us-ascii all +* thread (3 2)(1) + +ok store 1 +flags \deleted +ok expunge + +ok thread references us-ascii all +* thread (2 1) diff --git a/tests/resources/imap-test/thread-orderedsubject b/tests/resources/imap-test/thread-orderedsubject new file mode 100644 index 00000000..850b17cb --- /dev/null +++ b/tests/resources/imap-test/thread-orderedsubject @@ -0,0 +1,5 @@ +capabilities: THREAD=ORDEREDSUBJECT +messages: all + +ok thread orderedsubject us-ascii all +* THREAD (1)(2 (7)(12)(13))(3 (5)(11))(4)(6)(8)(9 10)(14)(15) diff --git a/tests/resources/imap-test/thread-orderedsubject.mbox b/tests/resources/imap-test/thread-orderedsubject.mbox new file mode 100644 index 00000000..79853fec --- /dev/null +++ b/tests/resources/imap-test/thread-orderedsubject.mbox @@ -0,0 +1,81 @@ +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: a + +1 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: C + +2 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: b + +3 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: _ + +4 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: [foo] Fwd: [bar] Re: fw: b (fWd) (fwd) + +55 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: b (a) + +66 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: Re: [FWD: c] + +77 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: [xyz] + +88 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 + +9 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: =?iso-8859-1?q?_?= + +10 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: Re: =?utf-8?q?b?= + +11 +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: =?iso-8859-1?q?RE:_C?= + +12 +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: =?us-ascii?b?UmU6IGM=?= + +13 + +The subject is 'Re: c'. +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: Ad: Re: Ad: Re: Ad: x + +14 + +From user@domain Fri Feb 22 23:00:00 2008 +0200 +Subject: re: [fwd: [fwd: re: [fwd: babylon]]] + +15 diff --git a/tests/resources/imap-test/thread-orderedsubject2 b/tests/resources/imap-test/thread-orderedsubject2 new file mode 100644 index 00000000..42f35b80 --- /dev/null +++ b/tests/resources/imap-test/thread-orderedsubject2 @@ -0,0 +1,5 @@ +capabilities: THREAD=ORDEREDSUBJECT +messages: all + +ok thread orderedsubject us-ascii all +* THREAD (4 (2)(8)(6))(3 (1)(7)(5)) diff --git a/tests/resources/imap-test/thread-orderedsubject2.mbox b/tests/resources/imap-test/thread-orderedsubject2.mbox new file mode 100644 index 00000000..95b66ca9 --- /dev/null +++ b/tests/resources/imap-test/thread-orderedsubject2.mbox @@ -0,0 +1,44 @@ +From user@domain Fri Feb 22 23:00:01 2008 +0200 +Subject: a +Date: Fri, 22 Feb 2008 22:00:10 +0200 + +1 + +From user@domain Fri Feb 22 23:00:02 2008 +0200 +Date: Fri, 22 Feb 2008 22:00:09 +0200 +Subject: b + +2 + +From user@domain Fri Feb 22 23:00:03 2008 +0200 +Date: Fri, 22 Feb 2008 22:00:08 +0200 +Subject: a + +3 + +From user@domain Fri Feb 22 23:00:04 2008 +0200 +Date: Fri, 22 Feb 2008 22:00:07 +0200 +Subject: b + +4 + +From user@domain Fri Feb 22 22:00:30 2008 +0200 +Subject: a + +5 + +From user@domain Fri Feb 22 22:00:29 2008 +0200 +Subject: b + +6 + +From user@domain Fri Feb 22 22:00:28 2008 +0200 +Subject: a + +7 + +From user@domain Fri Feb 22 22:00:27 2008 +0200 +Subject: b + +8 + diff --git a/tests/resources/imap-test/thread.mbox b/tests/resources/imap-test/thread.mbox new file mode 100644 index 00000000..deeace1f --- /dev/null +++ b/tests/resources/imap-test/thread.mbox @@ -0,0 +1,16 @@ +From user@domain Fri Feb 22 17:06:23 2008 +Message-ID: + +body + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: +References: + +body + +From user@domain Fri Feb 22 16:06:25 2008 +Message-Id: +References: + +body diff --git a/tests/resources/imap-test/thread2 b/tests/resources/imap-test/thread2 new file mode 100644 index 00000000..6041e24a --- /dev/null +++ b/tests/resources/imap-test/thread2 @@ -0,0 +1,5 @@ +capabilities: THREAD=REFERENCES +messages: all + +ok thread references us-ascii all +* thread (1)(2) diff --git a/tests/resources/imap-test/thread2.mbox b/tests/resources/imap-test/thread2.mbox new file mode 100644 index 00000000..f0ca2cf2 --- /dev/null +++ b/tests/resources/imap-test/thread2.mbox @@ -0,0 +1,9 @@ +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: + +body + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: + +body diff --git a/tests/resources/imap-test/thread3 b/tests/resources/imap-test/thread3 new file mode 100644 index 00000000..160e4d7b --- /dev/null +++ b/tests/resources/imap-test/thread3 @@ -0,0 +1,5 @@ +capabilities: THREAD=REFERENCES +messages: all + +ok thread references us-ascii all +* THREAD (1 2) diff --git a/tests/resources/imap-test/thread3.mbox b/tests/resources/imap-test/thread3.mbox new file mode 100644 index 00000000..02971a6a --- /dev/null +++ b/tests/resources/imap-test/thread3.mbox @@ -0,0 +1,11 @@ +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: +Subject: foo + +body + +From user@domain Fri Feb 22 15:06:24 2008 +Message-Id: +Subject: Re: foo + +body diff --git a/tests/resources/imap-test/thread4 b/tests/resources/imap-test/thread4 new file mode 100644 index 00000000..160e4d7b --- /dev/null +++ b/tests/resources/imap-test/thread4 @@ -0,0 +1,5 @@ +capabilities: THREAD=REFERENCES +messages: all + +ok thread references us-ascii all +* THREAD (1 2) diff --git a/tests/resources/imap-test/thread4.mbox b/tests/resources/imap-test/thread4.mbox new file mode 100644 index 00000000..c0ab7425 --- /dev/null +++ b/tests/resources/imap-test/thread4.mbox @@ -0,0 +1,12 @@ +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: +Subject: foo + +body + +From user@domain Fri Feb 22 15:06:24 2008 +Message-Id: +References: +Subject: Re: foo + +body diff --git a/tests/resources/imap-test/thread5 b/tests/resources/imap-test/thread5 new file mode 100644 index 00000000..83169ad5 --- /dev/null +++ b/tests/resources/imap-test/thread5 @@ -0,0 +1,20 @@ +capabilities: THREAD=REFERENCES +state: created + +ok append +ok append +ok append +ok append + +ok select $mailbox +ok thread references us-ascii all +* THREAD (1 (2 4)(3)) + +ok store 1,2 +flags \deleted +ok expunge +ok thread references us-ascii all +* THREAD (1)(2) + +ok append +ok thread references us-ascii all +* THREAD (1)((2)(3)) diff --git a/tests/resources/imap-test/thread5.mbox b/tests/resources/imap-test/thread5.mbox new file mode 100644 index 00000000..bfd45882 --- /dev/null +++ b/tests/resources/imap-test/thread5.mbox @@ -0,0 +1,29 @@ +From user@domain Fri Feb 22 17:06:23 2008 +Message-ID: <1@b> + +body + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: <2@b> +In-Reply-To: <1@b> + +body + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: <3@b> +In-Reply-To: <1@b> + +body + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: <4@b> +In-Reply-To: <2@b> + +body + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: <5@b> +In-Reply-To: <2@b> + +body + diff --git a/tests/resources/imap-test/thread6 b/tests/resources/imap-test/thread6 new file mode 100644 index 00000000..22596029 --- /dev/null +++ b/tests/resources/imap-test/thread6 @@ -0,0 +1,5 @@ +capabilities: THREAD=REFERENCES +messages: all + +ok thread references us-ascii all +* THREAD ((1)(2)) diff --git a/tests/resources/imap-test/thread6.mbox b/tests/resources/imap-test/thread6.mbox new file mode 100644 index 00000000..8ec4ab4b --- /dev/null +++ b/tests/resources/imap-test/thread6.mbox @@ -0,0 +1,9 @@ +From user@domain Fri Feb 22 15:06:23 2008 +References: + +body + +From user@domain Fri Feb 22 15:06:24 2008 +References: + +body diff --git a/tests/resources/imap-test/thread7 b/tests/resources/imap-test/thread7 new file mode 100644 index 00000000..5c6e2b91 --- /dev/null +++ b/tests/resources/imap-test/thread7 @@ -0,0 +1,11 @@ +capabilities: THREAD=REFERENCES +messages: all + +ok thread references us-ascii all +* thread (1 (2 3)(4)) + +ok store 2 +flags \deleted +ok expunge + +ok thread references us-ascii all +* thread (1 3 2) diff --git a/tests/resources/imap-test/thread7.mbox b/tests/resources/imap-test/thread7.mbox new file mode 100644 index 00000000..c5620d96 --- /dev/null +++ b/tests/resources/imap-test/thread7.mbox @@ -0,0 +1,22 @@ +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: + +body1 + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: +References: + +body2 + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: +References: + +body3 + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: +References: + +body4 (2 duplicate) diff --git a/tests/resources/imap-test/thread8 b/tests/resources/imap-test/thread8 new file mode 100644 index 00000000..1c68cd37 --- /dev/null +++ b/tests/resources/imap-test/thread8 @@ -0,0 +1,20 @@ +capabilities: THREAD=REFERENCES +state: created + +ok append +ok append +ok select $mailbox + +ok thread references us-ascii all +* thread (1 2) + +ok store 2 +flags \deleted +ok expunge + +ok thread references us-ascii all +* thread (1) + +ok append + +ok thread references us-ascii all +* thread (1 2) diff --git a/tests/resources/imap-test/thread8.mbox b/tests/resources/imap-test/thread8.mbox new file mode 100644 index 00000000..d319e8a8 --- /dev/null +++ b/tests/resources/imap-test/thread8.mbox @@ -0,0 +1,16 @@ +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: + +body1 + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: +References: + +body2 + +From user@domain Fri Feb 22 15:06:23 2008 +Message-Id: +References: + +body3 diff --git a/tests/resources/imap-test/uidplus b/tests/resources/imap-test/uidplus new file mode 100644 index 00000000..a1f1c6d9 --- /dev/null +++ b/tests/resources/imap-test/uidplus @@ -0,0 +1,23 @@ +capabilities: UIDPLUS +state: created + +ok select ${mailbox} +ok append + +"" delete ${mailbox}2 +ok create ${mailbox}2 + +append ${mailbox}2 +ok [appenduid $uidvalidity $uid] + +copy 1 ${mailbox}2 +ok [copyuid $uidvalidity $srcuid $uid2] + +ok select ${mailbox}2 +* ok [uidvalidity $uidvalidity] +ok fetch 1:2 uid +* 1 fetch (uid $uid) +* 2 fetch (uid $uid2) + +ok close +"" delete ${mailbox}2 diff --git a/tests/resources/imap-test/uidvalidity b/tests/resources/imap-test/uidvalidity new file mode 100644 index 00000000..15787cb3 --- /dev/null +++ b/tests/resources/imap-test/uidvalidity @@ -0,0 +1,13 @@ +connections: 2 +state: created + +1 ok append +1 ok status $mailbox (uidvalidity uidnext) +* status $mailbox (uidvalidity $uidvalidity uidnext $uidnext) +1 ok delete $mailbox + +2 ok create $mailbox +2 ok append $mailbox + +#1 ok status $mailbox (uidvalidity uidnext) +#! status $mailbox (uidvalidity $uidvalidity uidnext $uidnext) diff --git a/tests/resources/imap-test/uidvalidity-rename b/tests/resources/imap-test/uidvalidity-rename new file mode 100644 index 00000000..f99c48de --- /dev/null +++ b/tests/resources/imap-test/uidvalidity-rename @@ -0,0 +1,19 @@ +connections: 2 +state: auth + +1 ok create ${mailbox} +2 ok create ${mailbox}2 + +1 ok append +2 ok append + +1 ok status $mailbox (uidvalidity uidnext) +* status $mailbox (uidvalidity $uidvalidity uidnext $uidnext) + +1 ok rename ${mailbox} ${mailbox}3 +2 ok rename ${mailbox}2 ${mailbox} + +#1 ok status $mailbox (uidvalidity uidnext) +#! status $mailbox (uidvalidity $uidvalidity uidnext $uidnext) + +1 "" delete ${mailbox}3 diff --git a/tests/resources/imap-test/urlauth b/tests/resources/imap-test/urlauth new file mode 100644 index 00000000..f68823f0 --- /dev/null +++ b/tests/resources/imap-test/urlauth @@ -0,0 +1,26 @@ +capabilities: urlauth +messages: 2 + +ok fetch 1:2 (uid body[]) +* 1 fetch (uid $uid1 body[] $body1) +* 2 fetch (uid $uid2 body[] $body2) + +ok GENURLAUTH "imap://$username@$domain/$mailbox_url/;uid=$uid1;urlauth=user+$user" INTERNAL +* GENURLAUTH $mail_url1 + +ok GENURLAUTH "imap://$username@$domain/$mailbox_url/;uid=$uid2;urlauth=user+$user" INTERNAL +* GENURLAUTH $mail_url2 + +ok URLFETCH $mail_url1 +* URLFETCH $mail_url1 $body1 + +ok URLFETCH $mail_url2 +* URLFETCH $mail_url2 $body2 + +ok resetkey $mailbox + +ok URLFETCH $mail_url1 +* URLFETCH $mail_url1 NIL + +ok URLFETCH $mail_url2 +* URLFETCH $mail_url2 NIL diff --git a/tests/resources/imap-test/urlauth-binary b/tests/resources/imap-test/urlauth-binary new file mode 100644 index 00000000..c7dd647d --- /dev/null +++ b/tests/resources/imap-test/urlauth-binary @@ -0,0 +1,88 @@ +capabilities: urlauth urlauth=binary +state: created + +ok select ${mailbox} +ok append +ok append + +ok fetch 1:2 uid +* 1 fetch (uid $uid1) +* 2 fetch (uid $uid2) + +ok GENURLAUTH "imap://$username@$domain/$mailbox_url/;uid=$uid1/;section=1;urlauth=user+$user" INTERNAL +* GENURLAUTH $mail_url1 +ok GENURLAUTH "imap://$username@$domain/$mailbox_url/;uid=$uid1/;section=1.1;urlauth=user+$user" INTERNAL +* GENURLAUTH $mail_url1sub + +ok GENURLAUTH "imap://$username@$domain/$mailbox_url/;uid=$uid2/;section=1;urlauth=user+$user" INTERNAL +* GENURLAUTH $mail_url2 +ok GENURLAUTH "imap://$username@$domain/$mailbox_url/;uid=$uid2/;section=1.1;urlauth=user+$user" INTERNAL +* GENURLAUTH $mail_url2sub + +ok URLFETCH ($mail_url1 binary) +* URLFETCH $mail_url1 (binary {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: binary + +hello world +--sub1 +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: binary + +hello world +--sub1-- +Sub MIME epilogue + +}}}) + +ok URLFETCH ($mail_url1sub binary) +* URLFETCH $mail_url1sub (binary {{{ +hello world +}}}) + +ok URLFETCH ($mail_url1 bodypartstructure) +* URLFETCH $mail_url1 (BODYPARTSTRUCTURE ("message" "rfc822" NIL NIL NIL "7bit" 437 ("Sun, 12 Aug 2012 12:34:56 +0300" "submsg" ((NIL NIL "sub" "domain.org")) ((NIL NIL "sub" "domain.org")) ((NIL NIL "sub" "domain.org")) NIL NIL NIL NIL NIL) (("text" "x-myown" ("charset" "us-ascii") NIL NIL "base64" 22 3 NIL NIL NIL NIL)("text" "x-myown" ("charset" "us-ascii") NIL NIL "base64" 47 5 NIL NIL NIL NIL) "alternative" ("boundary" "sub1") NIL NIL NIL) 26 NIL NIL NIL NIL)) + +ok URLFETCH ($mail_url1sub bodypartstructure) +* URLFETCH $mail_url1sub (BODYPARTSTRUCTURE ("text" "x-myown" ("charset" "us-ascii") NIL NIL "base64" 22 3 NIL NIL NIL NIL)) + + +ok URLFETCH ($mail_url2 binary bodypartstructure) +* URLFETCH $mail_url2 (BODYPARTSTRUCTURE ("message" "rfc822" NIL NIL NIL "7bit" 390 ("Sun, 12 Aug 2012 12:34:56 +0300" "submsg" ((NIL NIL "sub" "domain.org")) ((NIL NIL "sub" "domain.org")) ((NIL NIL "sub" "domain.org")) NIL NIL NIL NIL NIL) (("text" "x-myown" ("charset" "us-ascii") NIL NIL "base64" 11 0 NIL NIL NIL NIL)("text" "x-myown" ("charset" "us-ascii") NIL NIL "base64" 11 0 NIL NIL NIL NIL) "alternative" ("boundary" "sub1") NIL NIL NIL) 18 NIL NIL NIL NIL) binary {{{ +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: binary + +hello world +--sub1 +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: binary + +hello world +--sub1-- +Sub MIME epilogue + +}}}) + +ok URLFETCH ($mail_url1sub binary) +* URLFETCH $mail_url1sub (binary {{{ +hello world +}}}) + +ok URLFETCH ($mail_url2sub binary bodypartstructure) +* URLFETCH $mail_url2sub (BODYPARTSTRUCTURE ("text" "x-myown" ("charset" "us-ascii") NIL NIL "base64" 11 0 NIL NIL NIL NIL) BINARY {{{ +hello world +}}}) diff --git a/tests/resources/imap-test/urlauth-binary.mbox b/tests/resources/imap-test/urlauth-binary.mbox new file mode 100644 index 00000000..576ddb80 --- /dev/null +++ b/tests/resources/imap-test/urlauth-binary.mbox @@ -0,0 +1,41 @@ +From user@domain Fri Feb 22 17:06:23 2008 +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + +Root MIME prologue + +--foo bar +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: base64 + +aGVs +bG8gd29y +bGQ= + +--sub1 +Content-Type: text/x-myown; charset=us-ascii +Content-Transfer-Encoding: base64 + + aGVs + bG8g + d29y + bGQ= + + +--sub1-- +Sub MIME epilogue + +--foo bar-- +Root MIME epilogue diff --git a/tests/resources/imap-test/urlauth2 b/tests/resources/imap-test/urlauth2 new file mode 100644 index 00000000..59be1c55 --- /dev/null +++ b/tests/resources/imap-test/urlauth2 @@ -0,0 +1,27 @@ +capabilities: urlauth +connections: 2 +user 2: $user2 + +1 ok fetch 1:2 (uid body[]) +* 1 fetch (uid $uid1 body[] $body1) +* 2 fetch (uid $uid2 body[] $body2) + +1 ok GENURLAUTH "imap://$username@$domain/$mailbox_url/;uid=$uid1;urlauth=authuser" INTERNAL +* GENURLAUTH $mail_url1 + +1 ok GENURLAUTH "imap://$username@$domain/$mailbox_url/;uid=$uid2;urlauth=authuser" INTERNAL +* GENURLAUTH $mail_url2 + +2 ok URLFETCH $mail_url1 +* URLFETCH $mail_url1 $body1 + +2 ok URLFETCH $mail_url2 +* URLFETCH $mail_url2 $body2 + +1 ok resetkey $mailbox + +2 ok URLFETCH $mail_url1 +* URLFETCH $mail_url1 NIL + +2 ok URLFETCH $mail_url2 +* URLFETCH $mail_url2 NIL diff --git a/tests/resources/imap/000.imap b/tests/resources/imap/000.imap new file mode 100644 index 00000000..9fc41fc8 --- /dev/null +++ b/tests/resources/imap/000.imap @@ -0,0 +1,482 @@ +BODY ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 262 7 + ) ( + "text" "plain" ( + "charset" "US-ASCII" + ) NIL NIL "7bit" 111 3 + ) ( + ( + "text" "basic" ( + "charset" "us-ascii" + ) NIL NIL "base64" 85 2 + ) ( + "text" "jpeg" ( + "charset" "us-ascii" + ) NIL NIL "base64" 44 1 + ) "parallel" + ) ( + "text" "enriched" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 140 5 + ) ( + "message" "rfc822" NIL NIL NIL NIL 223 ( + NIL "(subject in US-ASCII)" NIL NIL NIL NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "ISO-8859-1" + ) NIL NIL "Quoted-printable" 48 1 + ) 0 + ) "mixed" +) + +BODYSTRUCTURE ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 262 7 "1ba9f4410aca245ffe18870d1767eaf1" NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "US-ASCII" + ) NIL NIL "7bit" 111 3 "457a84e4830817334703ecead7a71bdf" NIL NIL NIL + ) ( + ( + "text" "basic" ( + "charset" "us-ascii" + ) NIL NIL "base64" 85 2 "c6e3404e683fdeb88e96dfed0abbddad" NIL NIL NIL + ) ( + "text" "jpeg" ( + "charset" "us-ascii" + ) NIL NIL "base64" 44 1 "88217c7613d6252c87ba0cd279f1e93c" NIL NIL NIL + ) "parallel" ( + "boundary" "unique-boundary-2" + ) NIL NIL NIL + ) ( + "text" "enriched" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 140 5 "4a5cd280a875e791bd6d767dcf60c43d" NIL NIL NIL + ) ( + "message" "rfc822" NIL NIL NIL NIL 223 ( + NIL "(subject in US-ASCII)" NIL NIL NIL NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "ISO-8859-1" + ) NIL NIL "Quoted-printable" 48 1 "632b0aae19d3c55d420c0dc8cebaf049" NIL NIL NIL + ) 0 "40ceb8762dcb7270c37f1395e91aa893" NIL NIL NIL + ) "mixed" ( + "boundary" "unique-boundary-1" + ) NIL NIL NIL +) + +BODY[] {1854} +MIME-Version: 1.0 +From: Nathaniel Borenstein +To: Ned Freed +Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT) +Subject: A multipart example +Content-Type: multipart/mixed; + boundary=unique-boundary-1 + +This is the preamble area of a multipart message. +Mail readers that understand multipart format +should ignore this preamble. + +If you are reading this text, you might want to +consider changing to a mail reader that understands +how to properly display multipart messages. + +--unique-boundary-1 + +... Some text appears here ... + +[Note that the blank between the boundary and the start +of the text in this part means no header fields were +given and this is text in the US-ASCII character set. +It could have been done with explicit typing as in the +next part.] + +--unique-boundary-1 +Content-type: text/plain; charset=US-ASCII + +This could have been part of the previous part, but +illustrates explicit versus implicit typing of body +parts. + +--unique-boundary-1 +Content-Type: multipart/parallel; boundary=unique-boundary-2 + +--unique-boundary-2 +Content-Type: audio/basic +Content-Transfer-Encoding: base64 + +... base64-encoded 8000 Hz single-channel + mu-law-format audio data goes here ... + +--unique-boundary-2 +Content-Type: image/jpeg +Content-Transfer-Encoding: base64 + +... base64-encoded image data goes here ... + +--unique-boundary-2-- + +--unique-boundary-1 +Content-type: text/enriched + +This is enriched. +as defined in RFC 1896 + +Isn't it +cool? + +--unique-boundary-1 +Content-Type: message/rfc822 + +From: (mailbox in US-ASCII) +To: (address in US-ASCII) +Subject: (subject in US-ASCII) +Content-Type: Text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: Quoted-printable + +... Additional text in ISO-8859-1 goes here ... + +--unique-boundary-1-- + + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {239} +MIME-Version: 1.0 +From: Nathaniel Borenstein +To: Ned Freed +Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT) +Subject: A multipart example +Content-Type: multipart/mixed; + boundary=unique-boundary-1 + + +---------------------------------- +BODY[TEXT] {1615} +This is the preamble area of a multipart message. +Mail readers that understand multipart format +should ignore this preamble. + +If you are reading this text, you might want to +consider changing to a mail reader that understands +how to properly display multipart messages. + +--unique-boundary-1 + +... Some text appears here ... + +[Note that the blank between the boundary and the start +of the text in this part means no header fields were +given and this is text in the US-ASCII character set. +It could have been done with explicit typing as in the +next part.] + +--unique-boundary-1 +Content-type: text/plain; charset=US-ASCII + +This could have been part of the previous part, but +illustrates explicit versus implicit typing of body +parts. + +--unique-boundary-1 +Content-Type: multipart/parallel; boundary=unique-boundary-2 + +--unique-boundary-2 +Content-Type: audio/basic +Content-Transfer-Encoding: base64 + +... base64-encoded 8000 Hz single-channel + mu-law-format audio data goes here ... + +--unique-boundary-2 +Content-Type: image/jpeg +Content-Transfer-Encoding: base64 + +... base64-encoded image data goes here ... + +--unique-boundary-2-- + +--unique-boundary-1 +Content-type: text/enriched + +This is enriched. +as defined in RFC 1896 + +Isn't it +cool? + +--unique-boundary-1 +Content-Type: message/rfc822 + +From: (mailbox in US-ASCII) +To: (address in US-ASCII) +Subject: (subject in US-ASCII) +Content-Type: Text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: Quoted-printable + +... Additional text in ISO-8859-1 goes here ... + +--unique-boundary-1-- + + +---------------------------------- +BODY[MIME] {72} +Content-Type: multipart/mixed; + boundary=unique-boundary-1 + + +---------------------------------- +BODY[1] {262} +... Some text appears here ... + +[Note that the blank between the boundary and the start +of the text in this part means no header fields were +given and this is text in the US-ASCII character set. +It could have been done with explicit typing as in the +next part.] + +BINARY[1] {262} +... Some text appears here ... + +[Note that the blank between the boundary and the start +of the text in this part means no header fields were +given and this is text in the US-ASCII character set. +It could have been done with explicit typing as in the +next part.] + +BINARY.SIZE[1] 262 +---------------------------------- +BODY[1.HEADER] {1} + + +---------------------------------- +BODY[1.TEXT] {262} +... Some text appears here ... + +[Note that the blank between the boundary and the start +of the text in this part means no header fields were +given and this is text in the US-ASCII character set. +It could have been done with explicit typing as in the +next part.] + +---------------------------------- +BODY[1.MIME] {2} + + +---------------------------------- +BODY[2] {111} +This could have been part of the previous part, but +illustrates explicit versus implicit typing of body +parts. + +BINARY[2] {111} +This could have been part of the previous part, but +illustrates explicit versus implicit typing of body +parts. + +BINARY.SIZE[2] 111 +---------------------------------- +BODY[2.HEADER] {44} +Content-type: text/plain; charset=US-ASCII + + +---------------------------------- +BODY[2.TEXT] {111} +This could have been part of the previous part, but +illustrates explicit versus implicit typing of body +parts. + +---------------------------------- +BODY[2.MIME] {45} +Content-Type: text/plain; charset=US-ASCII + + +---------------------------------- +BODY[3] {314} +--unique-boundary-2 +Content-Type: audio/basic +Content-Transfer-Encoding: base64 + +... base64-encoded 8000 Hz single-channel + mu-law-format audio data goes here ... + +--unique-boundary-2 +Content-Type: image/jpeg +Content-Transfer-Encoding: base64 + +... base64-encoded image data goes here ... + +--unique-boundary-2-- + +BINARY.SIZE[3] 0 +---------------------------------- +BODY[3.HEADER] {62} +Content-Type: multipart/parallel; boundary=unique-boundary-2 + + +---------------------------------- +BODY[3.TEXT] {314} +--unique-boundary-2 +Content-Type: audio/basic +Content-Transfer-Encoding: base64 + +... base64-encoded 8000 Hz single-channel + mu-law-format audio data goes here ... + +--unique-boundary-2 +Content-Type: image/jpeg +Content-Transfer-Encoding: base64 + +... base64-encoded image data goes here ... + +--unique-boundary-2-- + +---------------------------------- +BODY[3.MIME] {63} +Content-Type: multipart/parallel; boundary=unique-boundary-2 + + +---------------------------------- +BODY[3.1] {85} +... base64-encoded 8000 Hz single-channel + mu-law-format audio data goes here ... + +* NO [UNKNOWN-CTE] Failed to decode part 3.1 of message 0. + +BINARY.SIZE[3.1] 85 +---------------------------------- +BODY[3.1.HEADER] {61} +Content-Type: audio/basic +Content-Transfer-Encoding: base64 + + +---------------------------------- +BODY[3.1.TEXT] {85} +... base64-encoded 8000 Hz single-channel + mu-law-format audio data goes here ... + +---------------------------------- +BODY[3.1.MIME] {62} +Content-Type: audio/basic +Content-Transfer-Encoding: base64 + + +---------------------------------- +BODY[3.2] {44} +... base64-encoded image data goes here ... + +* NO [UNKNOWN-CTE] Failed to decode part 3.2 of message 0. + +BINARY.SIZE[3.2] 44 +---------------------------------- +BODY[3.2.HEADER] {60} +Content-Type: image/jpeg +Content-Transfer-Encoding: base64 + + +---------------------------------- +BODY[3.2.TEXT] {44} +... base64-encoded image data goes here ... + +---------------------------------- +BODY[3.2.MIME] {61} +Content-Type: image/jpeg +Content-Transfer-Encoding: base64 + + +---------------------------------- +BODY[4] {140} +This is enriched. +as defined in RFC 1896 + +Isn't it +cool? + +BINARY[4] {140} +This is enriched. +as defined in RFC 1896 + +Isn't it +cool? + +BINARY.SIZE[4] 140 +---------------------------------- +BODY[4.HEADER] {29} +Content-type: text/enriched + + +---------------------------------- +BODY[4.TEXT] {140} +This is enriched. +as defined in RFC 1896 + +Isn't it +cool? + +---------------------------------- +BODY[4.MIME] {30} +Content-Type: text/enriched + + +---------------------------------- +BODY[5] {223} +From: (mailbox in US-ASCII) +To: (address in US-ASCII) +Subject: (subject in US-ASCII) +Content-Type: Text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: Quoted-printable + +... Additional text in ISO-8859-1 goes here ... + +BINARY[5] {16} +[binary content] +BINARY.SIZE[5] 223 +---------------------------------- +BODY[5.HEADER] {175} +From: (mailbox in US-ASCII) +To: (address in US-ASCII) +Subject: (subject in US-ASCII) +Content-Type: Text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: Quoted-printable + + +---------------------------------- +BODY[5.TEXT] {48} +... Additional text in ISO-8859-1 goes here ... + +---------------------------------- +BODY[5.MIME] {91} +Content-Type: Text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: Quoted-printable + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {79} +From: Nathaniel Borenstein +To: Ned Freed + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {25} +aniel Borenstein +To: Ned Freed +Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT) +Content-Type: multipart/mixed; + boundary=unique-boundary-1 + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +on: 1.0 +From: Nathaniel B +---------------------------------- diff --git a/tests/resources/imap/000.txt b/tests/resources/imap/000.txt new file mode 100644 index 00000000..44a15885 --- /dev/null +++ b/tests/resources/imap/000.txt @@ -0,0 +1,73 @@ +MIME-Version: 1.0 +From: Nathaniel Borenstein +To: Ned Freed +Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT) +Subject: A multipart example +Content-Type: multipart/mixed; + boundary=unique-boundary-1 + +This is the preamble area of a multipart message. +Mail readers that understand multipart format +should ignore this preamble. + +If you are reading this text, you might want to +consider changing to a mail reader that understands +how to properly display multipart messages. + +--unique-boundary-1 + +... Some text appears here ... + +[Note that the blank between the boundary and the start +of the text in this part means no header fields were +given and this is text in the US-ASCII character set. +It could have been done with explicit typing as in the +next part.] + +--unique-boundary-1 +Content-type: text/plain; charset=US-ASCII + +This could have been part of the previous part, but +illustrates explicit versus implicit typing of body +parts. + +--unique-boundary-1 +Content-Type: multipart/parallel; boundary=unique-boundary-2 + +--unique-boundary-2 +Content-Type: audio/basic +Content-Transfer-Encoding: base64 + +... base64-encoded 8000 Hz single-channel + mu-law-format audio data goes here ... + +--unique-boundary-2 +Content-Type: image/jpeg +Content-Transfer-Encoding: base64 + +... base64-encoded image data goes here ... + +--unique-boundary-2-- + +--unique-boundary-1 +Content-type: text/enriched + +This is enriched. +as defined in RFC 1896 + +Isn't it +cool? + +--unique-boundary-1 +Content-Type: message/rfc822 + +From: (mailbox in US-ASCII) +To: (address in US-ASCII) +Subject: (subject in US-ASCII) +Content-Type: Text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: Quoted-printable + +... Additional text in ISO-8859-1 goes here ... + +--unique-boundary-1-- + diff --git a/tests/resources/imap/001.imap b/tests/resources/imap/001.imap new file mode 100644 index 00000000..6f707de6 --- /dev/null +++ b/tests/resources/imap/001.imap @@ -0,0 +1,251 @@ +BODY ( + ( + "message" "external-body" ( + "name" "BodyFormats.ps" "site" "thumper.bellcore.com" "mode" "image" "access-type" "ANON-FTP" "directory" "pub" "expiration" "Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + ) NIL NIL NIL 79 + ) ( + "message" "external-body" ( + "access-type" "local-file" "name" "/u/nsb/writing/rfcs/RFC-MIME.ps" "site" "thumper.bellcore.com" "expiration" "Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + ) NIL NIL NIL 79 + ) ( + "message" "external-body" ( + "access-type" "mail-server" "server" "listserv@bogus.bitnet" "expiration" "Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + ) NIL NIL NIL 97 + ) "alternative" +) + +BODYSTRUCTURE ( + ( + "message" "external-body" ( + "name" "BodyFormats.ps" "site" "thumper.bellcore.com" "mode" "image" "access-type" "ANON-FTP" "directory" "pub" "expiration" "Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + ) NIL NIL NIL 79 "13a120642a010037cbc238c999349116" NIL NIL NIL + ) ( + "message" "external-body" ( + "access-type" "local-file" "name" "/u/nsb/writing/rfcs/RFC-MIME.ps" "site" "thumper.bellcore.com" "expiration" "Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + ) NIL NIL NIL 79 "13a120642a010037cbc238c999349116" NIL NIL NIL + ) ( + "message" "external-body" ( + "access-type" "mail-server" "server" "listserv@bogus.bitnet" "expiration" "Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + ) NIL NIL NIL 97 "6f6c2486f2516ed45542404973c1cebb" NIL NIL NIL + ) "alternative" ( + "boundary" "42" + ) NIL NIL NIL +) + +BODY[] {1109} +From: Whomever +To: Someone +Date: Whenever +Subject: whatever +MIME-Version: 1.0 +Message-ID: +Content-Type: multipart/alternative; boundary=42 +Content-ID: + +--42 +Content-Type: message/external-body; name="BodyFormats.ps"; + site="thumper.bellcore.com"; mode="image"; + access-type=ANON-FTP; directory="pub"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + +Content-type: application/postscript +Content-ID: + +--42 +Content-Type: message/external-body; access-type=local-file; + name="/u/nsb/writing/rfcs/RFC-MIME.ps"; + site="thumper.bellcore.com"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + +Content-type: application/postscript +Content-ID: + +--42 +Content-Type: message/external-body; + access-type=mail-server + server="listserv@bogus.bitnet"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + +Content-type: application/postscript +Content-ID: + +get RFC-MIME.DOC + +--42-- + + + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {198} +From: Whomever +To: Someone +Date: Whenever +Subject: whatever +MIME-Version: 1.0 +Message-ID: +Content-Type: multipart/alternative; boundary=42 +Content-ID: + + +---------------------------------- +BODY[TEXT] {911} +--42 +Content-Type: message/external-body; name="BodyFormats.ps"; + site="thumper.bellcore.com"; mode="image"; + access-type=ANON-FTP; directory="pub"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + +Content-type: application/postscript +Content-ID: + +--42 +Content-Type: message/external-body; access-type=local-file; + name="/u/nsb/writing/rfcs/RFC-MIME.ps"; + site="thumper.bellcore.com"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + +Content-type: application/postscript +Content-ID: + +--42 +Content-Type: message/external-body; + access-type=mail-server + server="listserv@bogus.bitnet"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + +Content-type: application/postscript +Content-ID: + +get RFC-MIME.DOC + +--42-- + + + +---------------------------------- +BODY[MIME] {94} +Content-Type: multipart/alternative; boundary=42 +Content-ID: + + +---------------------------------- +BODY[1] {79} +Content-type: application/postscript +Content-ID: + +BINARY[1] {16} +[binary content] +BINARY.SIZE[1] 79 +---------------------------------- +BODY[1.HEADER] {230} +Content-Type: message/external-body; name="BodyFormats.ps"; + site="thumper.bellcore.com"; mode="image"; + access-type=ANON-FTP; directory="pub"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + + +---------------------------------- +BODY[1.TEXT] {79} +Content-type: application/postscript +Content-ID: + +---------------------------------- +BODY[1.MIME] {231} +Content-Type: message/external-body; name="BodyFormats.ps"; + site="thumper.bellcore.com"; mode="image"; + access-type=ANON-FTP; directory="pub"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + + +---------------------------------- +BODY[2] {79} +Content-type: application/postscript +Content-ID: + +BINARY[2] {16} +[binary content] +BINARY.SIZE[2] 79 +---------------------------------- +BODY[2.HEADER] {218} +Content-Type: message/external-body; access-type=local-file; + name="/u/nsb/writing/rfcs/RFC-MIME.ps"; + site="thumper.bellcore.com"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + + +---------------------------------- +BODY[2.TEXT] {79} +Content-type: application/postscript +Content-ID: + +---------------------------------- +BODY[2.MIME] {219} +Content-Type: message/external-body; access-type=local-file; + name="/u/nsb/writing/rfcs/RFC-MIME.ps"; + site="thumper.bellcore.com"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + + +---------------------------------- +BODY[3] {97} +Content-type: application/postscript +Content-ID: + +get RFC-MIME.DOC + +BINARY[3] {16} +[binary content] +BINARY.SIZE[3] 97 +---------------------------------- +BODY[3.HEADER] {181} +Content-Type: message/external-body; + access-type=mail-server + server="listserv@bogus.bitnet"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + + +---------------------------------- +BODY[3.TEXT] {97} +Content-type: application/postscript +Content-ID: + +get RFC-MIME.DOC + +---------------------------------- +BODY[3.MIME] {182} +Content-Type: message/external-body; + access-type=mail-server + server="listserv@bogus.bitnet"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {29} +From: Whomever +To: Someone + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {19} +ever +To: Someone + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)] {181} +From: Whomever +To: Someone +Date: Whenever +MIME-Version: 1.0 +Message-ID: +Content-Type: multipart/alternative; boundary=42 +Content-ID: + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +ever +To: Someone +Date: Wh +---------------------------------- diff --git a/tests/resources/imap/001.txt b/tests/resources/imap/001.txt new file mode 100644 index 00000000..8b4abc86 --- /dev/null +++ b/tests/resources/imap/001.txt @@ -0,0 +1,41 @@ +From: Whomever +To: Someone +Date: Whenever +Subject: whatever +MIME-Version: 1.0 +Message-ID: +Content-Type: multipart/alternative; boundary=42 +Content-ID: + +--42 +Content-Type: message/external-body; name="BodyFormats.ps"; + site="thumper.bellcore.com"; mode="image"; + access-type=ANON-FTP; directory="pub"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + +Content-type: application/postscript +Content-ID: + +--42 +Content-Type: message/external-body; access-type=local-file; + name="/u/nsb/writing/rfcs/RFC-MIME.ps"; + site="thumper.bellcore.com"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + +Content-type: application/postscript +Content-ID: + +--42 +Content-Type: message/external-body; + access-type=mail-server + server="listserv@bogus.bitnet"; + expiration="Fri, 14 Jun 1991 19:13:14 -0400 (EDT)" + +Content-type: application/postscript +Content-ID: + +get RFC-MIME.DOC + +--42-- + + diff --git a/tests/resources/imap/002.imap b/tests/resources/imap/002.imap new file mode 100644 index 00000000..9acc14d1 --- /dev/null +++ b/tests/resources/imap/002.imap @@ -0,0 +1,510 @@ +BODY ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 61 5 + ) ( + "message" "rfc822" NIL NIL NIL "7bit" 1979 ( + "Thu, 13 Aug 1998 07:42:41 +0000" "Map of Argentina with Description" ( + ( + "Bill Clinton" NIL "president" "whitehouse.gov" + ) + ) ( + ( + "Bill Clinton" NIL "president" "whitehouse.gov" + ) + ) ( + ( + "Bill Clinton" NIL "president" "whitehouse.gov" + ) + ) ( + ( + "A1 Gore (The Enforcer)" NIL "vice-president" "whitehouse.gov" + ) + ) NIL NIL NIL "<199804130742.RAA20366@mai1host.whitehouse.gov>" + ) ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 355 12 + ) ( + "image" "gif" ( + "name" "map_of_Argentina.gif" + ) NIL NIL "base64" 389 + ) "mixed" + ) 0 + ) "mixed" +) + +BODYSTRUCTURE ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 61 5 "63aa55d7eacd3e7d4af551d3d7f8fe46" NIL NIL NIL + ) ( + "message" "rfc822" NIL NIL NIL "7bit" 1979 ( + "Thu, 13 Aug 1998 07:42:41 +0000" "Map of Argentina with Description" ( + ( + "Bill Clinton" NIL "president" "whitehouse.gov" + ) + ) ( + ( + "Bill Clinton" NIL "president" "whitehouse.gov" + ) + ) ( + ( + "Bill Clinton" NIL "president" "whitehouse.gov" + ) + ) ( + ( + "A1 Gore (The Enforcer)" NIL "vice-president" "whitehouse.gov" + ) + ) NIL NIL NIL "<199804130742.RAA20366@mai1host.whitehouse.gov>" + ) ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 355 12 "47027313db254547987c7eb4be377593" NIL NIL NIL + ) ( + "image" "gif" ( + "name" "map_of_Argentina.gif" + ) NIL NIL "base64" 389 "5359a63540c1a8eae06a27e8760e1501" ( + "inline" ( + "fi1ename" "map_of_Argentina.gif" + ) + ) NIL NIL + ) "mixed" ( + "boundary" "DC8------------DC8638F443D87A7F0726DEF7" + ) NIL NIL NIL + ) 0 "1522079b4114146b37ea1af171484a1a" ( + "inline" ( + + ) + ) NIL NIL + ) "mixed" ( + "boundary" "D7F------------D7FD5A0B8AB9C65CCDBFA872" + ) NIL NIL NIL +) + +BODY[] {2652} +From: Al Gore +To: White House Transportation Coordinator + +Subject: [Fwd: Map of Argentina with Description] +Content-Type: multipart/mixed; + boundary="D7F------------D7FD5A0B8AB9C65CCDBFA872" + +This is a multi-part message in MIME format. +--D7F------------D7FD5A0B8AB9C65CCDBFA872 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Fred, + +Fire up Air Force One! We're going South! + +Thanks, +Al +--D7F------------D7FD5A0B8AB9C65CCDBFA872 +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +Return-Path: +Received: from mailhost.whitehouse.gov ([192.168.51.200]) + by heartbeat.whitehouse.gov (8.8.8/8.8.8) with ESMTP id SAA22453 + for ; + Mon, 13 Aug 1998 l8:14:23 +1000 +Received: from the_big_box.whitehouse.gov ([192.168.51.50]) + by mailhost.whitehouse.gov (8.8.8/8.8.7) with ESMTP id RAA20366 + for vice-president@whitehouse.gov; Mon, 13 Aug 1998 17:42:41 +1000 +Date: Mon, 13 Aug 1998 17:42:41 +1000 +Message-Id: <199804130742.RAA20366@mai1host.whitehouse.gov> +From: Bill Clinton +To: A1 (The Enforcer) Gore +Subject: Map of Argentina with Description +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="DC8------------DC8638F443D87A7F0726DEF7" + +This is a multi-part message in MIME format. +--DC8------------DC8638F443D87A7F0726DEF7 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill +--DC8------------DC8638F443D87A7F0726DEF7 +Content-Type: image/gif; name="map_of_Argentina.gif" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; fi1ename="map_of_Argentina.gif" + +R01GOD1hJQA1AKIAAP/////78P/omn19fQAAAAAAAAAAAAAAACwAAAAAJQA1AAAD7Qi63P5w +wEmjBCLrnQnhYCgM1wh+pkgqqeC9XrutmBm7hAK3tP31gFcAiFKVQrGFR6kscnonTe7FAAad +GugmRu3CmiBt57fsVq3Y0VFKnpYdxPC6M7Ze4crnnHum4oN6LFJ1bn5NXTN7OF5fQkN5WYow +BEN2dkGQGWJtSzqGTICJgnQuTJN/WJsojad9qXMuhIWdjXKjY4tenjo6tjVssk2gaWq3uGNX +U6ZGxseyk8SasGw3J9GRzdTQky1iHNvcPNNI4TLeKdfMvy0vMqLrItvuxfDW8ubjueDtJufz +7itICBxISKDBgwgTKjyYAAA7 +--DC8------------DC8638F443D87A7F0726DEF7-- + +--D7F------------D7FD5A0B8AB9C65CCDBFA872-- + + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {270} +From: Al Gore +To: White House Transportation Coordinator + +Subject: [Fwd: Map of Argentina with Description] +Content-Type: multipart/mixed; + boundary="D7F------------D7FD5A0B8AB9C65CCDBFA872" + + +---------------------------------- +BODY[TEXT] {2382} +This is a multi-part message in MIME format. +--D7F------------D7FD5A0B8AB9C65CCDBFA872 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Fred, + +Fire up Air Force One! We're going South! + +Thanks, +Al +--D7F------------D7FD5A0B8AB9C65CCDBFA872 +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +Return-Path: +Received: from mailhost.whitehouse.gov ([192.168.51.200]) + by heartbeat.whitehouse.gov (8.8.8/8.8.8) with ESMTP id SAA22453 + for ; + Mon, 13 Aug 1998 l8:14:23 +1000 +Received: from the_big_box.whitehouse.gov ([192.168.51.50]) + by mailhost.whitehouse.gov (8.8.8/8.8.7) with ESMTP id RAA20366 + for vice-president@whitehouse.gov; Mon, 13 Aug 1998 17:42:41 +1000 +Date: Mon, 13 Aug 1998 17:42:41 +1000 +Message-Id: <199804130742.RAA20366@mai1host.whitehouse.gov> +From: Bill Clinton +To: A1 (The Enforcer) Gore +Subject: Map of Argentina with Description +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="DC8------------DC8638F443D87A7F0726DEF7" + +This is a multi-part message in MIME format. +--DC8------------DC8638F443D87A7F0726DEF7 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill +--DC8------------DC8638F443D87A7F0726DEF7 +Content-Type: image/gif; name="map_of_Argentina.gif" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; fi1ename="map_of_Argentina.gif" + +R01GOD1hJQA1AKIAAP/////78P/omn19fQAAAAAAAAAAAAAAACwAAAAAJQA1AAAD7Qi63P5w +wEmjBCLrnQnhYCgM1wh+pkgqqeC9XrutmBm7hAK3tP31gFcAiFKVQrGFR6kscnonTe7FAAad +GugmRu3CmiBt57fsVq3Y0VFKnpYdxPC6M7Ze4crnnHum4oN6LFJ1bn5NXTN7OF5fQkN5WYow +BEN2dkGQGWJtSzqGTICJgnQuTJN/WJsojad9qXMuhIWdjXKjY4tenjo6tjVssk2gaWq3uGNX +U6ZGxseyk8SasGw3J9GRzdTQky1iHNvcPNNI4TLeKdfMvy0vMqLrItvuxfDW8ubjueDtJufz +7itICBxISKDBgwgTKjyYAAA7 +--DC8------------DC8638F443D87A7F0726DEF7-- + +--D7F------------D7FD5A0B8AB9C65CCDBFA872-- + + +---------------------------------- +BODY[MIME] {98} +Content-Type: multipart/mixed; + boundary="D7F------------D7FD5A0B8AB9C65CCDBFA872" + + +---------------------------------- +BODY[1] {61} +Fred, + +Fire up Air Force One! We're going South! + +Thanks, +Al +BINARY[1] {61} +Fred, + +Fire up Air Force One! We're going South! + +Thanks, +Al +BINARY.SIZE[1] 61 +---------------------------------- +BODY[1.HEADER] {76} +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[1.TEXT] {61} +Fred, + +Fire up Air Force One! We're going South! + +Thanks, +Al +---------------------------------- +BODY[1.MIME] {77} +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[2] {1979} +Return-Path: +Received: from mailhost.whitehouse.gov ([192.168.51.200]) + by heartbeat.whitehouse.gov (8.8.8/8.8.8) with ESMTP id SAA22453 + for ; + Mon, 13 Aug 1998 l8:14:23 +1000 +Received: from the_big_box.whitehouse.gov ([192.168.51.50]) + by mailhost.whitehouse.gov (8.8.8/8.8.7) with ESMTP id RAA20366 + for vice-president@whitehouse.gov; Mon, 13 Aug 1998 17:42:41 +1000 +Date: Mon, 13 Aug 1998 17:42:41 +1000 +Message-Id: <199804130742.RAA20366@mai1host.whitehouse.gov> +From: Bill Clinton +To: A1 (The Enforcer) Gore +Subject: Map of Argentina with Description +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="DC8------------DC8638F443D87A7F0726DEF7" + +This is a multi-part message in MIME format. +--DC8------------DC8638F443D87A7F0726DEF7 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill +--DC8------------DC8638F443D87A7F0726DEF7 +Content-Type: image/gif; name="map_of_Argentina.gif" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; fi1ename="map_of_Argentina.gif" + +R01GOD1hJQA1AKIAAP/////78P/omn19fQAAAAAAAAAAAAAAACwAAAAAJQA1AAAD7Qi63P5w +wEmjBCLrnQnhYCgM1wh+pkgqqeC9XrutmBm7hAK3tP31gFcAiFKVQrGFR6kscnonTe7FAAad +GugmRu3CmiBt57fsVq3Y0VFKnpYdxPC6M7Ze4crnnHum4oN6LFJ1bn5NXTN7OF5fQkN5WYow +BEN2dkGQGWJtSzqGTICJgnQuTJN/WJsojad9qXMuhIWdjXKjY4tenjo6tjVssk2gaWq3uGNX +U6ZGxseyk8SasGw3J9GRzdTQky1iHNvcPNNI4TLeKdfMvy0vMqLrItvuxfDW8ubjueDtJufz +7itICBxISKDBgwgTKjyYAAA7 +--DC8------------DC8638F443D87A7F0726DEF7-- + +BINARY[2] {16} +[binary content] +BINARY.SIZE[2] 1979 +---------------------------------- +BODY[2.HEADER] {835} +Return-Path: +Received: from mailhost.whitehouse.gov ([192.168.51.200]) + by heartbeat.whitehouse.gov (8.8.8/8.8.8) with ESMTP id SAA22453 + for ; + Mon, 13 Aug 1998 l8:14:23 +1000 +Received: from the_big_box.whitehouse.gov ([192.168.51.50]) + by mailhost.whitehouse.gov (8.8.8/8.8.7) with ESMTP id RAA20366 + for vice-president@whitehouse.gov; Mon, 13 Aug 1998 17:42:41 +1000 +Date: Mon, 13 Aug 1998 17:42:41 +1000 +Message-Id: <199804130742.RAA20366@mai1host.whitehouse.gov> +From: Bill Clinton +To: A1 (The Enforcer) Gore +Subject: Map of Argentina with Description +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="DC8------------DC8638F443D87A7F0726DEF7" + + +---------------------------------- +BODY[2.TEXT] {1144} +This is a multi-part message in MIME format. +--DC8------------DC8638F443D87A7F0726DEF7 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill +--DC8------------DC8638F443D87A7F0726DEF7 +Content-Type: image/gif; name="map_of_Argentina.gif" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; fi1ename="map_of_Argentina.gif" + +R01GOD1hJQA1AKIAAP/////78P/omn19fQAAAAAAAAAAAAAAACwAAAAAJQA1AAAD7Qi63P5w +wEmjBCLrnQnhYCgM1wh+pkgqqeC9XrutmBm7hAK3tP31gFcAiFKVQrGFR6kscnonTe7FAAad +GugmRu3CmiBt57fsVq3Y0VFKnpYdxPC6M7Ze4crnnHum4oN6LFJ1bn5NXTN7OF5fQkN5WYow +BEN2dkGQGWJtSzqGTICJgnQuTJN/WJsojad9qXMuhIWdjXKjY4tenjo6tjVssk2gaWq3uGNX +U6ZGxseyk8SasGw3J9GRzdTQky1iHNvcPNNI4TLeKdfMvy0vMqLrItvuxfDW8ubjueDtJufz +7itICBxISKDBgwgTKjyYAAA7 +--DC8------------DC8638F443D87A7F0726DEF7-- + +---------------------------------- +BODY[2.MIME] {98} +Content-Type: multipart/mixed; + boundary="DC8------------DC8638F443D87A7F0726DEF7" + + +---------------------------------- +BODY[2.1] {355} +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill +BINARY[2.1] {355} +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill +BINARY.SIZE[2.1] 355 +---------------------------------- +BODY[2.1.HEADER] {76} +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[2.1.TEXT] {355} +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill +---------------------------------- +BODY[2.1.MIME] {77} +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[2.2] {389} +R01GOD1hJQA1AKIAAP/////78P/omn19fQAAAAAAAAAAAAAAACwAAAAAJQA1AAAD7Qi63P5w +wEmjBCLrnQnhYCgM1wh+pkgqqeC9XrutmBm7hAK3tP31gFcAiFKVQrGFR6kscnonTe7FAAad +GugmRu3CmiBt57fsVq3Y0VFKnpYdxPC6M7Ze4crnnHum4oN6LFJ1bn5NXTN7OF5fQkN5WYow +BEN2dkGQGWJtSzqGTICJgnQuTJN/WJsojad9qXMuhIWdjXKjY4tenjo6tjVssk2gaWq3uGNX +U6ZGxseyk8SasGw3J9GRzdTQky1iHNvcPNNI4TLeKdfMvy0vMqLrItvuxfDW8ubjueDtJufz +7itICBxISKDBgwgTKjyYAAA7 +BINARY[2.2] {16} +[binary content] +BINARY.SIZE[2.2] 288 +---------------------------------- +BODY[2.2.HEADER] {149} +Content-Type: image/gif; name="map_of_Argentina.gif" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; fi1ename="map_of_Argentina.gif" + + +---------------------------------- +BODY[2.2.TEXT] {389} +R01GOD1hJQA1AKIAAP/////78P/omn19fQAAAAAAAAAAAAAAACwAAAAAJQA1AAAD7Qi63P5w +wEmjBCLrnQnhYCgM1wh+pkgqqeC9XrutmBm7hAK3tP31gFcAiFKVQrGFR6kscnonTe7FAAad +GugmRu3CmiBt57fsVq3Y0VFKnpYdxPC6M7Ze4crnnHum4oN6LFJ1bn5NXTN7OF5fQkN5WYow +BEN2dkGQGWJtSzqGTICJgnQuTJN/WJsojad9qXMuhIWdjXKjY4tenjo6tjVssk2gaWq3uGNX +U6ZGxseyk8SasGw3J9GRzdTQky1iHNvcPNNI4TLeKdfMvy0vMqLrItvuxfDW8ubjueDtJufz +7itICBxISKDBgwgTKjyYAAA7 +---------------------------------- +BODY[2.2.MIME] {150} +Content-Type: image/gif; name="map_of_Argentina.gif" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; fi1ename="map_of_Argentina.gif" + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {125} +From: Al Gore +To: White House Transportation Coordinator + + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {25} +Gore +To: White House Transportation Coordinator + +Content-Type: multipart/mixed; + boundary="D7F------------D7FD5A0B8AB9C65CCDBFA872" + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +Gore +To: White House Transportation Coordinator + +Subject: [Fwd: Map of Argentina with Description] +Content-Type: multipart/mixed; + boundary="D7F------------D7FD5A0B8AB9C65CCDBFA872" + +This is a multi-part message in MIME format. +--D7F------------D7FD5A0B8AB9C65CCDBFA872 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Fred, + +Fire up Air Force One! We're going South! + +Thanks, +Al +--D7F------------D7FD5A0B8AB9C65CCDBFA872 +Content-Type: message/rfc822 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +Return-Path: +Received: from mailhost.whitehouse.gov ([192.168.51.200]) + by heartbeat.whitehouse.gov (8.8.8/8.8.8) with ESMTP id SAA22453 + for ; + Mon, 13 Aug 1998 l8:14:23 +1000 +Received: from the_big_box.whitehouse.gov ([192.168.51.50]) + by mailhost.whitehouse.gov (8.8.8/8.8.7) with ESMTP id RAA20366 + for vice-president@whitehouse.gov; Mon, 13 Aug 1998 17:42:41 +1000 +Date: Mon, 13 Aug 1998 17:42:41 +1000 +Message-Id: <199804130742.RAA20366@mai1host.whitehouse.gov> +From: Bill Clinton +To: A1 (The Enforcer) Gore +Subject: Map of Argentina with Description +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="DC8------------DC8638F443D87A7F0726DEF7" + +This is a multi-part message in MIME format. +--DC8------------DC8638F443D87A7F0726DEF7 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill +--DC8------------DC8638F443D87A7F0726DEF7 +Content-Type: image/gif; name="map_of_Argentina.gif" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; fi1ename="map_of_Argentina.gif" + +R01GOD1hJQA1AKIAAP/////78P/omn19fQAAAAAAAAAAAAAAACwAAAAAJQA1AAAD7Qi63P5w +wEmjBCLrnQnhYCgM1wh+pkgqqeC9XrutmBm7hAK3tP31gFcAiFKVQrGFR6kscnonTe7FAAad +GugmRu3CmiBt57fsVq3Y0VFKnpYdxPC6M7Ze4crnnHum4oN6LFJ1bn5NXTN7OF5fQkN5WYow +BEN2dkGQGWJtSzqGTICJgnQuTJN/WJsojad9qXMuhIWdjXKjY4tenjo6tjVssk2gaWq3uGNX +U6ZGxseyk8SasGw3J9GRzdTQky1iHNvcPNNI4TLeKdfMvy0vMqLrItvuxfDW8ubjueDtJufz +7itICBxISKDBgwgTKjyYAAA7 +--DC8------------DC8638F443D87A7F0726DEF7-- + +--D7F------------D7FD5A0B8AB9C65CCDBFA872-- + diff --git a/tests/resources/imap/003.imap b/tests/resources/imap/003.imap new file mode 100644 index 00000000..596b82e6 --- /dev/null +++ b/tests/resources/imap/003.imap @@ -0,0 +1,184 @@ +BODY ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 48 1 + ) ( + "text" "enriched" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 69 2 + ) ( + "application" "x-whatever" NIL NIL NIL NIL 51 + ) "alternative" +) + +BODYSTRUCTURE ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 48 1 "2229d79e5de40eae37c43fe934adbd24" NIL NIL NIL + ) ( + "text" "enriched" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 69 2 "1aba0fd91c6544b8626008f78b83c3f9" NIL NIL NIL + ) ( + "application" "x-whatever" NIL NIL NIL NIL 51 "7f7ef645554f637854b8acdd853aa612" NIL NIL NIL + ) "alternative" ( + "boundary" "boundary42" + ) NIL NIL NIL +) + +BODY[] {565} +From: Nathaniel Borenstein +To: Ned Freed +Date: Mon, 22 Mar 1993 09:41:09 -0800 (PST) +Subject: Formatted text mail +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary=boundary42 + +--boundary42 +Content-Type: text/plain; charset=us-ascii + +... plain text version of message goes here ... + +--boundary42 +Content-Type: text/enriched + +... RFC 1896 text/enriched version of same message + goes here ... + +--boundary42 +Content-Type: application/x-whatever + +... fanciest version of same message goes here ... + +--boundary42-- + + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {228} +From: Nathaniel Borenstein +To: Ned Freed +Date: Mon, 22 Mar 1993 09:41:09 -0800 (PST) +Subject: Formatted text mail +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary=boundary42 + + +---------------------------------- +BODY[TEXT] {337} +--boundary42 +Content-Type: text/plain; charset=us-ascii + +... plain text version of message goes here ... + +--boundary42 +Content-Type: text/enriched + +... RFC 1896 text/enriched version of same message + goes here ... + +--boundary42 +Content-Type: application/x-whatever + +... fanciest version of same message goes here ... + +--boundary42-- + + +---------------------------------- +BODY[MIME] {59} +Content-Type: multipart/alternative; boundary=boundary42 + + +---------------------------------- +BODY[1] {48} +... plain text version of message goes here ... + +BINARY[1] {48} +... plain text version of message goes here ... + +BINARY.SIZE[1] 48 +---------------------------------- +BODY[1.HEADER] {44} +Content-Type: text/plain; charset=us-ascii + + +---------------------------------- +BODY[1.TEXT] {48} +... plain text version of message goes here ... + +---------------------------------- +BODY[1.MIME] {45} +Content-Type: text/plain; charset=us-ascii + + +---------------------------------- +BODY[2] {69} +... RFC 1896 text/enriched version of same message + goes here ... + +BINARY[2] {69} +... RFC 1896 text/enriched version of same message + goes here ... + +BINARY.SIZE[2] 69 +---------------------------------- +BODY[2.HEADER] {29} +Content-Type: text/enriched + + +---------------------------------- +BODY[2.TEXT] {69} +... RFC 1896 text/enriched version of same message + goes here ... + +---------------------------------- +BODY[2.MIME] {30} +Content-Type: text/enriched + + +---------------------------------- +BODY[3] {51} +... fanciest version of same message goes here ... + +BINARY[3] {16} +[binary content] +BINARY.SIZE[3] 51 +---------------------------------- +BODY[3.HEADER] {38} +Content-Type: application/x-whatever + + +---------------------------------- +BODY[3.TEXT] {51} +... fanciest version of same message goes here ... + +---------------------------------- +BODY[3.MIME] {39} +Content-Type: application/x-whatever + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {81} +From: Nathaniel Borenstein +To: Ned Freed + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {25} +aniel Borenstein +To: Ned Freed +Date: Mon, 22 Mar 1993 09:41:09 -0800 (PST) +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary=boundary42 + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +aniel Borenstein +To: Ned Freed +Date: Mon, 22 Mar 1993 09:41:09 -0800 (PST) +Subject: Formatted text mail +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary=boundary42 + +--boundary42 +Content-Type: text/plain; charset=us-ascii + +... plain text version of message goes here ... + +--boundary42 +Content-Type: text/enriched + +... RFC 1896 text/enriched version of same message + goes here ... + +--boundary42 +Content-Type: application/x-whatever + +... fanciest version of same message goes here ... + +--boundary42-- + diff --git a/tests/resources/imap/004.imap b/tests/resources/imap/004.imap new file mode 100644 index 00000000..6164036c --- /dev/null +++ b/tests/resources/imap/004.imap @@ -0,0 +1,293 @@ +BODY ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 45 1 + ) ( + ( + "message" NIL NIL NIL NIL NIL 100 ( + "Fri, 26 Mar 1993 09:13:32 +0000" "my opinion" NIL NIL NIL NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 22 1 + ) 0 + ) ( + "message" NIL NIL NIL NIL NIL 125 ( + "Fri, 26 Mar 1993 15:07:13 +0000" "my different opinion" NIL NIL NIL NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 31 1 + ) 0 + ) "digest" + ) "mixed" +) + +BODYSTRUCTURE ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 45 1 "f7f7e917e54763330bc648984459b38e" NIL NIL NIL + ) ( + ( + "message" NIL NIL NIL NIL NIL 100 ( + "Fri, 26 Mar 1993 09:13:32 +0000" "my opinion" NIL NIL NIL NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 22 1 "1ec4e1f0175dc661a12854b33879ce9b" NIL NIL NIL + ) 0 "ef1527984c599523c90fb184a8fdfd61" NIL NIL NIL + ) ( + "message" NIL NIL NIL NIL NIL 125 ( + "Fri, 26 Mar 1993 15:07:13 +0000" "my different opinion" NIL NIL NIL NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 31 1 "7a0001277b1197e7029ad48b54919b62" NIL NIL NIL + ) 0 "9609bccef55fba1208046885cdcc9db8" NIL NIL NIL + ) "digest" ( + "boundary" "---- next message ----" + ) NIL NIL NIL + ) "mixed" ( + "boundary" "---- main boundary ----" + ) NIL NIL NIL +) + +BODY[] {728} +From: Moderator-Address +To: Recipient-List +Date: Mon, 22 Mar 1994 13:34:51 +0000 +Subject: Internet Digest, volume 42 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="---- main boundary ----" + +------ main boundary ---- + +...Introductory text or table of contents... + +------ main boundary ---- +Content-Type: multipart/digest; + boundary="---- next message ----" + +------ next message ---- + +From: someone-else +Date: Fri, 26 Mar 1993 11:13:32 +0200 +Subject: my opinion + +...body goes here ... + +------ next message ---- + +From: someone-else-again +Date: Fri, 26 Mar 1993 10:07:13 -0500 +Subject: my different opinion + +... another body goes here ... + +------ next message ------ + +------ main boundary ------ + + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {214} +From: Moderator-Address +To: Recipient-List +Date: Mon, 22 Mar 1994 13:34:51 +0000 +Subject: Internet Digest, volume 42 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="---- main boundary ----" + + +---------------------------------- +BODY[TEXT] {514} +------ main boundary ---- + +...Introductory text or table of contents... + +------ main boundary ---- +Content-Type: multipart/digest; + boundary="---- next message ----" + +------ next message ---- + +From: someone-else +Date: Fri, 26 Mar 1993 11:13:32 +0200 +Subject: my opinion + +...body goes here ... + +------ next message ---- + +From: someone-else-again +Date: Fri, 26 Mar 1993 10:07:13 -0500 +Subject: my different opinion + +... another body goes here ... + +------ next message ------ + +------ main boundary ------ + + +---------------------------------- +BODY[MIME] {80} +Content-Type: multipart/mixed; + boundary="---- main boundary ----" + + +---------------------------------- +BODY[1] {45} +...Introductory text or table of contents... + +BINARY[1] {45} +...Introductory text or table of contents... + +BINARY.SIZE[1] 45 +---------------------------------- +BODY[1.HEADER] {1} + + +---------------------------------- +BODY[1.TEXT] {45} +...Introductory text or table of contents... + +---------------------------------- +BODY[1.MIME] {2} + + +---------------------------------- +BODY[2] {306} +------ next message ---- + +From: someone-else +Date: Fri, 26 Mar 1993 11:13:32 +0200 +Subject: my opinion + +...body goes here ... + +------ next message ---- + +From: someone-else-again +Date: Fri, 26 Mar 1993 10:07:13 -0500 +Subject: my different opinion + +... another body goes here ... + +------ next message ------ + +BINARY.SIZE[2] 0 +---------------------------------- +BODY[2.HEADER] {79} +Content-Type: multipart/digest; + boundary="---- next message ----" + + +---------------------------------- +BODY[2.TEXT] {306} +------ next message ---- + +From: someone-else +Date: Fri, 26 Mar 1993 11:13:32 +0200 +Subject: my opinion + +...body goes here ... + +------ next message ---- + +From: someone-else-again +Date: Fri, 26 Mar 1993 10:07:13 -0500 +Subject: my different opinion + +... another body goes here ... + +------ next message ------ + +---------------------------------- +BODY[2.MIME] {80} +Content-Type: multipart/digest; + boundary="---- next message ----" + + +---------------------------------- +BODY[2.1] {100} +From: someone-else +Date: Fri, 26 Mar 1993 11:13:32 +0200 +Subject: my opinion + +...body goes here ... + +BINARY[2.1] {16} +[binary content] +BINARY.SIZE[2.1] 100 +---------------------------------- +BODY[2.1.HEADER] {78} +From: someone-else +Date: Fri, 26 Mar 1993 11:13:32 +0200 +Subject: my opinion + + +---------------------------------- +BODY[2.1.TEXT] {22} +...body goes here ... + +---------------------------------- +BODY[2.1.MIME] {2} + + +---------------------------------- +BODY[2.2] {125} +From: someone-else-again +Date: Fri, 26 Mar 1993 10:07:13 -0500 +Subject: my different opinion + +... another body goes here ... + +BINARY[2.2] {16} +[binary content] +BINARY.SIZE[2.2] 125 +---------------------------------- +BODY[2.2.HEADER] {94} +From: someone-else-again +Date: Fri, 26 Mar 1993 10:07:13 -0500 +Subject: my different opinion + + +---------------------------------- +BODY[2.2.TEXT] {31} +... another body goes here ... + +---------------------------------- +BODY[2.2.MIME] {2} + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {45} +From: Moderator-Address +To: Recipient-List + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {25} +rator-Address +To: Recipie +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)] {179} +From: Moderator-Address +To: Recipient-List +Date: Mon, 22 Mar 1994 13:34:51 +0000 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="---- main boundary ----" + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +rator-Address +To: Recipie +---------------------------------- diff --git a/tests/resources/imap/004.txt b/tests/resources/imap/004.txt new file mode 100644 index 00000000..55aebc04 --- /dev/null +++ b/tests/resources/imap/004.txt @@ -0,0 +1,36 @@ +From: Moderator-Address +To: Recipient-List +Date: Mon, 22 Mar 1994 13:34:51 +0000 +Subject: Internet Digest, volume 42 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="---- main boundary ----" + +------ main boundary ---- + +...Introductory text or table of contents... + +------ main boundary ---- +Content-Type: multipart/digest; + boundary="---- next message ----" + +------ next message ---- + +From: someone-else +Date: Fri, 26 Mar 1993 11:13:32 +0200 +Subject: my opinion + +...body goes here ... + +------ next message ---- + +From: someone-else-again +Date: Fri, 26 Mar 1993 10:07:13 -0500 +Subject: my different opinion + +... another body goes here ... + +------ next message ------ + +------ main boundary ------ + diff --git a/tests/resources/imap/005.imap b/tests/resources/imap/005.imap new file mode 100644 index 00000000..44ed69c9 --- /dev/null +++ b/tests/resources/imap/005.imap @@ -0,0 +1,157 @@ +BODY ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 79 1 + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 76 2 + ) "mixed" +) + +BODYSTRUCTURE ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 79 1 "b35878dedb7cd0aa6934f90df9d517b0" NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 76 2 "d3905bfd2a53ad0a4438d01bf271677a" NIL NIL NIL + ) "mixed" ( + "boundary" "simple boundary" + ) NIL NIL NIL +) + +BODY[] {691} +From: Nathaniel Borenstein +To: Ned Freed +Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST) +Subject: Sample message +MIME-Version: 1.0 +Content-type: multipart/mixed; boundary="simple boundary" + +This is the preamble. It is to be ignored, though it +is a handy place for composition agents to include an +explanatory note to non-MIME conformant readers. + +--simple boundary + +This is implicitly typed plain US-ASCII text. +It does NOT end with a linebreak. +--simple boundary +Content-type: text/plain; charset=us-ascii + +This is explicitly typed plain US-ASCII text. +It DOES end with a linebreak. + +--simple boundary-- + +This is the epilogue. It is also to be ignored. + + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {224} +From: Nathaniel Borenstein +To: Ned Freed +Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST) +Subject: Sample message +MIME-Version: 1.0 +Content-type: multipart/mixed; boundary="simple boundary" + + +---------------------------------- +BODY[TEXT] {467} +This is the preamble. It is to be ignored, though it +is a handy place for composition agents to include an +explanatory note to non-MIME conformant readers. + +--simple boundary + +This is implicitly typed plain US-ASCII text. +It does NOT end with a linebreak. +--simple boundary +Content-type: text/plain; charset=us-ascii + +This is explicitly typed plain US-ASCII text. +It DOES end with a linebreak. + +--simple boundary-- + +This is the epilogue. It is also to be ignored. + + +---------------------------------- +BODY[MIME] {60} +Content-Type: multipart/mixed; boundary="simple boundary" + + +---------------------------------- +BODY[1] {79} +This is implicitly typed plain US-ASCII text. +It does NOT end with a linebreak. +BINARY[1] {79} +This is implicitly typed plain US-ASCII text. +It does NOT end with a linebreak. +BINARY.SIZE[1] 79 +---------------------------------- +BODY[1.HEADER] {1} + + +---------------------------------- +BODY[1.TEXT] {79} +This is implicitly typed plain US-ASCII text. +It does NOT end with a linebreak. +---------------------------------- +BODY[1.MIME] {2} + + +---------------------------------- +BODY[2] {76} +This is explicitly typed plain US-ASCII text. +It DOES end with a linebreak. + +BINARY[2] {76} +This is explicitly typed plain US-ASCII text. +It DOES end with a linebreak. + +BINARY.SIZE[2] 76 +---------------------------------- +BODY[2.HEADER] {44} +Content-type: text/plain; charset=us-ascii + + +---------------------------------- +BODY[2.TEXT] {76} +This is explicitly typed plain US-ASCII text. +It DOES end with a linebreak. + +---------------------------------- +BODY[2.MIME] {45} +Content-Type: text/plain; charset=us-ascii + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {81} +From: Nathaniel Borenstein +To: Ned Freed + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {25} +aniel Borenstein +To: Ned Freed +Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST) +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="simple boundary" + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +aniel Borenstein +To: Ned Freed +Date: Sun, 21 Mar 1993 23:56:48 -0800 (PST) +Subject: Sample message +MIME-Version: 1.0 +Content-type: multipart/mixed; boundary="simple boundary" + +This is the preamble. It is to be ignored, though it +is a handy place for composition agents to include an +explanatory note to non-MIME conformant readers. + +--simple boundary + +This is implicitly typed plain US-ASCII text. +It does NOT end with a linebreak. +--simple boundary +Content-type: text/plain; charset=us-ascii + +This is explicitly typed plain US-ASCII text. +It DOES end with a linebreak. + +--simple boundary-- + +This is the epilogue. It is also to be ignored. + diff --git a/tests/resources/imap/006.imap b/tests/resources/imap/006.imap new file mode 100644 index 00000000..9af46b9e --- /dev/null +++ b/tests/resources/imap/006.imap @@ -0,0 +1,174 @@ +BODY ( + ( + "text" "plain" ( + "charset" "utf-8" + ) NIL NIL "quoted-printable" 87 2 + ) ( + "text" "html" ( + "charset" "utf-8" + ) NIL NIL "quoted-printable" 93 2 + ) "alternative" +) + +BODYSTRUCTURE ( + ( + "text" "plain" ( + "charset" "utf-8" + ) NIL NIL "quoted-printable" 87 2 "ba404f7e85d80b41eddae75d5087186d" ( + "inline" ( + + ) + ) NIL NIL + ) ( + "text" "html" ( + "charset" "utf-8" + ) NIL NIL "quoted-printable" 93 2 "bb2724458c0a6183195d922d711523c0" ( + "inline" ( + + ) + ) NIL NIL + ) "alternative" ( + "boundary" "boundary-string" + ) NIL NIL NIL +) + +BODY[] {617} +From: sender@example.com +To: recipient@example.com +Subject: Multipart Email Example +Content-Type: multipart/alternative; boundary="boundary-string" + +--boundary-string +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +Plain text email goes here! +This is the fallback if email client does not support HTML + +--boundary-string +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +

This is the HTML Section!

+

This is what displays in most modern email clients

+ +--boundary-string-- + + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {149} +From: sender@example.com +To: recipient@example.com +Subject: Multipart Email Example +Content-Type: multipart/alternative; boundary="boundary-string" + + +---------------------------------- +BODY[TEXT] {468} +--boundary-string +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +Plain text email goes here! +This is the fallback if email client does not support HTML + +--boundary-string +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +

This is the HTML Section!

+

This is what displays in most modern email clients

+ +--boundary-string-- + + +---------------------------------- +BODY[MIME] {66} +Content-Type: multipart/alternative; boundary="boundary-string" + + +---------------------------------- +BODY[1] {87} +Plain text email goes here! +This is the fallback if email client does not support HTML + +BINARY[1] {87} +Plain text email goes here! +This is the fallback if email client does not support HTML + +BINARY.SIZE[1] 87 +---------------------------------- +BODY[1.HEADER] {115} +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + + +---------------------------------- +BODY[1.TEXT] {87} +Plain text email goes here! +This is the fallback if email client does not support HTML + +---------------------------------- +BODY[1.MIME] {116} +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + + +---------------------------------- +BODY[2] {93} +

This is the HTML Section!

+

This is what displays in most modern email clients

+ +BINARY[2] {93} +

This is the HTML Section!

+

This is what displays in most modern email clients

+ +BINARY.SIZE[2] 93 +---------------------------------- +BODY[2.HEADER] {114} +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + + +---------------------------------- +BODY[2.TEXT] {93} +

This is the HTML Section!

+

This is what displays in most modern email clients

+ +---------------------------------- +BODY[2.MIME] {115} +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {53} +From: sender@example.com +To: recipient@example.com + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {25} +er@example.com +To: recipi +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)] {117} +From: sender@example.com +To: recipient@example.com +Content-Type: multipart/alternative; boundary="boundary-string" + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +er@example.com +To: recipi +---------------------------------- diff --git a/tests/resources/imap/006.txt b/tests/resources/imap/006.txt new file mode 100644 index 00000000..f353adb6 --- /dev/null +++ b/tests/resources/imap/006.txt @@ -0,0 +1,23 @@ +From: sender@example.com +To: recipient@example.com +Subject: Multipart Email Example +Content-Type: multipart/alternative; boundary="boundary-string" + +--boundary-string +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +Plain text email goes here! +This is the fallback if email client does not support HTML + +--boundary-string +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + +

This is the HTML Section!

+

This is what displays in most modern email clients

+ +--boundary-string-- + diff --git a/tests/resources/imap/007.imap b/tests/resources/imap/007.imap new file mode 100644 index 00000000..6d423b6e --- /dev/null +++ b/tests/resources/imap/007.imap @@ -0,0 +1,803 @@ +BODY ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 1 0 + ) ( + ( + ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 1 0 + ) ( + "image" "jpeg" NIL NIL NIL NIL 1 + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 1 0 + ) "mixed" + ) ( + ( + "text" "html" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 14 0 + ) ( + "image" "jpeg" NIL NIL NIL NIL 1 + ) "related" + ) "alternative" + ) ( + "image" "jpeg" NIL NIL NIL NIL 1 + ) ( + "application" "x-excel" NIL NIL NIL NIL 1 + ) ( + "message" "rfc822" NIL NIL NIL NIL 13 ( + NIL "J" NIL NIL NIL NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 1 0 + ) 0 + ) "mixed" + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 1 0 + ) "mixed" +) + +BODYSTRUCTURE ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 1 0 "7fc56270e7a70fa81a5935b72eacbe29" ( + "inline" ( + + ) + ) NIL NIL + ) ( + ( + ( + ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 1 0 "9d5ed678fe57bcca610140957afab571" ( + "inline" ( + + ) + ) NIL NIL + ) ( + "image" "jpeg" NIL NIL NIL NIL 1 "0d61f8370cad1d412f80b84d143e1257" ( + "inline" ( + + ) + ) NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 1 0 "f623e75af30e62bbd73d6df5b50bb7b5" ( + "inline" ( + + ) + ) NIL NIL + ) "mixed" ( + "boundary" "4" + ) NIL NIL NIL + ) ( + ( + "text" "html" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 14 0 "ece7293a99c7e12f8d044b683e5c2f33" NIL NIL NIL + ) ( + "image" "jpeg" NIL NIL NIL NIL 1 "800618943025315f869e4e1f09471012" NIL NIL NIL + ) "related" ( + "boundary" "5" + ) NIL NIL NIL + ) "alternative" ( + "boundary" "3" + ) NIL NIL NIL + ) ( + "image" "jpeg" NIL NIL NIL NIL 1 "dfcf28d0734569a6a693bc8194de62bf" ( + "attachment" ( + + ) + ) NIL NIL + ) ( + "application" "x-excel" NIL NIL NIL NIL 1 "c1d9f50f86825a1a2302ec2449c17196" NIL NIL NIL + ) ( + "message" "rfc822" NIL NIL NIL NIL 13 ( + NIL "J" NIL NIL NIL NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 1 0 "ff44570aca8241914870afbc310cdb85" NIL NIL NIL + ) 0 "9c9888d1b2c167dd33f7542df5a65aa7" NIL NIL NIL + ) "mixed" ( + "boundary" "2" + ) NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 1 0 "a5f3c6a11b03839d46af9fb43c97c188" ( + "inline" ( + + ) + ) NIL NIL + ) "mixed" ( + "boundary" "1" + ) NIL NIL NIL +) + +BODY[] {871} +Subject: RFC 8621 Section 4.1.4 test +Content-Type: multipart/mixed; boundary="1" + +--1 +Content-Type: text/plain +Content-Disposition: inline + +A +--1 +Content-Type: multipart/mixed; boundary="2" + +--2 +Content-Type: multipart/alternative; boundary="3" + +--3 +Content-Type: multipart/mixed; boundary="4" + +--4 +Content-Type: text/plain +Content-Disposition: inline + +B +--4 +Content-Type: image/jpeg +Content-Disposition: inline + +C +--4 +Content-Type: text/plain +Content-Disposition: inline + +D +--4-- + +--3 +Content-Type: multipart/related; boundary="5" + +--5 +Content-Type: text/html + +E +--5 +Content-Type: image/jpeg + +F +--5-- + +--3-- + +--2 +Content-Type: image/jpeg +Content-Disposition: attachment + +G +--2 +Content-Type: application/x-excel + +H +--2 +Content-Type: message/rfc822 + +Subject: J + +J +--2-- + +--1 +Content-Type: text/plain +Content-Disposition: inline + +K +--1-- + + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {82} +Subject: RFC 8621 Section 4.1.4 test +Content-Type: multipart/mixed; boundary="1" + + +---------------------------------- +BODY[TEXT] {789} +--1 +Content-Type: text/plain +Content-Disposition: inline + +A +--1 +Content-Type: multipart/mixed; boundary="2" + +--2 +Content-Type: multipart/alternative; boundary="3" + +--3 +Content-Type: multipart/mixed; boundary="4" + +--4 +Content-Type: text/plain +Content-Disposition: inline + +B +--4 +Content-Type: image/jpeg +Content-Disposition: inline + +C +--4 +Content-Type: text/plain +Content-Disposition: inline + +D +--4-- + +--3 +Content-Type: multipart/related; boundary="5" + +--5 +Content-Type: text/html + +E +--5 +Content-Type: image/jpeg + +F +--5-- + +--3-- + +--2 +Content-Type: image/jpeg +Content-Disposition: attachment + +G +--2 +Content-Type: application/x-excel + +H +--2 +Content-Type: message/rfc822 + +Subject: J + +J +--2-- + +--1 +Content-Type: text/plain +Content-Disposition: inline + +K +--1-- + + +---------------------------------- +BODY[MIME] {46} +Content-Type: multipart/mixed; boundary="1" + + +---------------------------------- +BODY[1] {1} +A +BINARY[1] {1} +A +BINARY.SIZE[1] 1 +---------------------------------- +BODY[1.HEADER] {54} +Content-Type: text/plain +Content-Disposition: inline + + +---------------------------------- +BODY[1.TEXT] {1} +A +---------------------------------- +BODY[1.MIME] {55} +Content-Type: text/plain +Content-Disposition: inline + + +---------------------------------- +BODY[2] {608} +--2 +Content-Type: multipart/alternative; boundary="3" + +--3 +Content-Type: multipart/mixed; boundary="4" + +--4 +Content-Type: text/plain +Content-Disposition: inline + +B +--4 +Content-Type: image/jpeg +Content-Disposition: inline + +C +--4 +Content-Type: text/plain +Content-Disposition: inline + +D +--4-- + +--3 +Content-Type: multipart/related; boundary="5" + +--5 +Content-Type: text/html + +E +--5 +Content-Type: image/jpeg + +F +--5-- + +--3-- + +--2 +Content-Type: image/jpeg +Content-Disposition: attachment + +G +--2 +Content-Type: application/x-excel + +H +--2 +Content-Type: message/rfc822 + +Subject: J + +J +--2-- + +BINARY.SIZE[2] 0 +---------------------------------- +BODY[2.HEADER] {45} +Content-Type: multipart/mixed; boundary="2" + + +---------------------------------- +BODY[2.TEXT] {608} +--2 +Content-Type: multipart/alternative; boundary="3" + +--3 +Content-Type: multipart/mixed; boundary="4" + +--4 +Content-Type: text/plain +Content-Disposition: inline + +B +--4 +Content-Type: image/jpeg +Content-Disposition: inline + +C +--4 +Content-Type: text/plain +Content-Disposition: inline + +D +--4-- + +--3 +Content-Type: multipart/related; boundary="5" + +--5 +Content-Type: text/html + +E +--5 +Content-Type: image/jpeg + +F +--5-- + +--3-- + +--2 +Content-Type: image/jpeg +Content-Disposition: attachment + +G +--2 +Content-Type: application/x-excel + +H +--2 +Content-Type: message/rfc822 + +Subject: J + +J +--2-- + +---------------------------------- +BODY[2.MIME] {46} +Content-Type: multipart/mixed; boundary="2" + + +---------------------------------- +BODY[2.1] {386} +--3 +Content-Type: multipart/mixed; boundary="4" + +--4 +Content-Type: text/plain +Content-Disposition: inline + +B +--4 +Content-Type: image/jpeg +Content-Disposition: inline + +C +--4 +Content-Type: text/plain +Content-Disposition: inline + +D +--4-- + +--3 +Content-Type: multipart/related; boundary="5" + +--5 +Content-Type: text/html + +E +--5 +Content-Type: image/jpeg + +F +--5-- + +--3-- + +BINARY.SIZE[2.1] 0 +---------------------------------- +BODY[2.1.HEADER] {51} +Content-Type: multipart/alternative; boundary="3" + + +---------------------------------- +BODY[2.1.TEXT] {386} +--3 +Content-Type: multipart/mixed; boundary="4" + +--4 +Content-Type: text/plain +Content-Disposition: inline + +B +--4 +Content-Type: image/jpeg +Content-Disposition: inline + +C +--4 +Content-Type: text/plain +Content-Disposition: inline + +D +--4-- + +--3 +Content-Type: multipart/related; boundary="5" + +--5 +Content-Type: text/html + +E +--5 +Content-Type: image/jpeg + +F +--5-- + +--3-- + +---------------------------------- +BODY[2.1.MIME] {52} +Content-Type: multipart/alternative; boundary="3" + + +---------------------------------- +BODY[2.1.1] {190} +--4 +Content-Type: text/plain +Content-Disposition: inline + +B +--4 +Content-Type: image/jpeg +Content-Disposition: inline + +C +--4 +Content-Type: text/plain +Content-Disposition: inline + +D +--4-- + +BINARY.SIZE[2.1.1] 0 +---------------------------------- +BODY[2.1.1.HEADER] {45} +Content-Type: multipart/mixed; boundary="4" + + +---------------------------------- +BODY[2.1.1.TEXT] {190} +--4 +Content-Type: text/plain +Content-Disposition: inline + +B +--4 +Content-Type: image/jpeg +Content-Disposition: inline + +C +--4 +Content-Type: text/plain +Content-Disposition: inline + +D +--4-- + +---------------------------------- +BODY[2.1.1.MIME] {46} +Content-Type: multipart/mixed; boundary="4" + + +---------------------------------- +BODY[2.1.1.1] {1} +B +BINARY[2.1.1.1] {1} +B +BINARY.SIZE[2.1.1.1] 1 +---------------------------------- +BODY[2.1.1.1.HEADER] {54} +Content-Type: text/plain +Content-Disposition: inline + + +---------------------------------- +BODY[2.1.1.1.TEXT] {1} +B +---------------------------------- +BODY[2.1.1.1.MIME] {55} +Content-Type: text/plain +Content-Disposition: inline + + +---------------------------------- +BODY[2.1.1.2] {1} +C +BINARY[2.1.1.2] {16} +[binary content] +BINARY.SIZE[2.1.1.2] 1 +---------------------------------- +BODY[2.1.1.2.HEADER] {54} +Content-Type: image/jpeg +Content-Disposition: inline + + +---------------------------------- +BODY[2.1.1.2.TEXT] {1} +C +---------------------------------- +BODY[2.1.1.2.MIME] {55} +Content-Type: image/jpeg +Content-Disposition: inline + + +---------------------------------- +BODY[2.1.1.3] {1} +D +BINARY[2.1.1.3] {1} +D +BINARY.SIZE[2.1.1.3] 1 +---------------------------------- +BODY[2.1.1.3.HEADER] {54} +Content-Type: text/plain +Content-Disposition: inline + + +---------------------------------- +BODY[2.1.1.3.TEXT] {1} +D +---------------------------------- +BODY[2.1.1.3.MIME] {55} +Content-Type: text/plain +Content-Disposition: inline + + +---------------------------------- +BODY[2.1.2] {86} +--5 +Content-Type: text/html + +E +--5 +Content-Type: image/jpeg + +F +--5-- + +BINARY.SIZE[2.1.2] 0 +---------------------------------- +BODY[2.1.2.HEADER] {47} +Content-Type: multipart/related; boundary="5" + + +---------------------------------- +BODY[2.1.2.TEXT] {86} +--5 +Content-Type: text/html + +E +--5 +Content-Type: image/jpeg + +F +--5-- + +---------------------------------- +BODY[2.1.2.MIME] {48} +Content-Type: multipart/related; boundary="5" + + +---------------------------------- +BODY[2.1.2.1] {14} +E +BINARY[2.1.2.1] {14} +E +BINARY.SIZE[2.1.2.1] 14 +---------------------------------- +BODY[2.1.2.1.HEADER] {25} +Content-Type: text/html + + +---------------------------------- +BODY[2.1.2.1.TEXT] {14} +E +---------------------------------- +BODY[2.1.2.1.MIME] {26} +Content-Type: text/html + + +---------------------------------- +BODY[2.1.2.2] {1} +F +BINARY[2.1.2.2] {16} +[binary content] +BINARY.SIZE[2.1.2.2] 1 +---------------------------------- +BODY[2.1.2.2.HEADER] {26} +Content-Type: image/jpeg + + +---------------------------------- +BODY[2.1.2.2.TEXT] {1} +F +---------------------------------- +BODY[2.1.2.2.MIME] {27} +Content-Type: image/jpeg + + +---------------------------------- +BODY[2.2] {1} +G +BINARY[2.2] {16} +[binary content] +BINARY.SIZE[2.2] 1 +---------------------------------- +BODY[2.2.HEADER] {58} +Content-Type: image/jpeg +Content-Disposition: attachment + + +---------------------------------- +BODY[2.2.TEXT] {1} +G +---------------------------------- +BODY[2.2.MIME] {59} +Content-Type: image/jpeg +Content-Disposition: attachment + + +---------------------------------- +BODY[2.3] {1} +H +BINARY[2.3] {16} +[binary content] +BINARY.SIZE[2.3] 1 +---------------------------------- +BODY[2.3.HEADER] {35} +Content-Type: application/x-excel + + +---------------------------------- +BODY[2.3.TEXT] {1} +H +---------------------------------- +BODY[2.3.MIME] {36} +Content-Type: application/x-excel + + +---------------------------------- +BODY[2.4] {13} +Subject: J + +J +BINARY[2.4] {16} +[binary content] +BINARY.SIZE[2.4] 13 +---------------------------------- +BODY[2.4.HEADER] {12} +Subject: J + + +---------------------------------- +BODY[2.4.TEXT] {1} +J +---------------------------------- +BODY[2.4.MIME] {2} + + +---------------------------------- +BODY[3] {1} +K +BINARY[3] {1} +K +BINARY.SIZE[3] 1 +---------------------------------- +BODY[3.HEADER] {54} +Content-Type: text/plain +Content-Disposition: inline + + +---------------------------------- +BODY[3.TEXT] {1} +K +---------------------------------- +BODY[3.MIME] {55} +Content-Type: text/plain +Content-Disposition: inline + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {2} + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {0} + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)] {46} +Content-Type: multipart/mixed; boundary="1" + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +pe: multipart/mixed; boun +---------------------------------- diff --git a/tests/resources/imap/007.txt b/tests/resources/imap/007.txt new file mode 100644 index 00000000..f70101d3 --- /dev/null +++ b/tests/resources/imap/007.txt @@ -0,0 +1,73 @@ +Subject: RFC 8621 Section 4.1.4 test +Content-Type: multipart/mixed; boundary="1" + +--1 +Content-Type: text/plain +Content-Disposition: inline + +A +--1 +Content-Type: multipart/mixed; boundary="2" + +--2 +Content-Type: multipart/alternative; boundary="3" + +--3 +Content-Type: multipart/mixed; boundary="4" + +--4 +Content-Type: text/plain +Content-Disposition: inline + +B +--4 +Content-Type: image/jpeg +Content-Disposition: inline + +C +--4 +Content-Type: text/plain +Content-Disposition: inline + +D +--4-- + +--3 +Content-Type: multipart/related; boundary="5" + +--5 +Content-Type: text/html + +E +--5 +Content-Type: image/jpeg + +F +--5-- + +--3-- + +--2 +Content-Type: image/jpeg +Content-Disposition: attachment + +G +--2 +Content-Type: application/x-excel + +H +--2 +Content-Type: message/rfc822 + +Subject: J + +J +--2-- + +--1 +Content-Type: text/plain +Content-Disposition: inline + +K +--1-- + diff --git a/tests/resources/imap/008.imap b/tests/resources/imap/008.imap new file mode 100644 index 00000000..4771eee9 --- /dev/null +++ b/tests/resources/imap/008.imap @@ -0,0 +1,328 @@ +BODY ( + ( + "text" "plain" ( + "charset" "utf-8" + ) NIL NIL "7bit" 54 0 + ) ( + "message" "rfc822" NIL NIL NIL "base64" 1179 ( + "Tue, 14 Dec 2021 10:48:25 +0000" "HTML test" ( + ( + "Name" NIL "email" "example.com" + ) + ) ( + ( + "Name" NIL "email" "example.com" + ) + ) ( + ( + "Name" NIL "email" "example.com" + ) + ) ( + ( + "email@example.com" NIL "email" "example.com" + ) + ) NIL NIL NIL "" + ) ( + ( + "text" "plain" ( + "charset" "utf-8" "format" "flowed" + ) NIL NIL "7bit" 30 0 + ) ( + "text" "html" ( + "charset" "utf-8" + ) NIL NIL "7bit" 173 8 + ) "alternative" + ) 0 + ) "mixed" +) + +BODYSTRUCTURE ( + ( + "text" "plain" ( + "charset" "utf-8" + ) NIL NIL "7bit" 54 0 "e377afc895a2c4c0d17b378f355de59e" NIL NIL NIL + ) ( + "message" "rfc822" NIL NIL NIL "base64" 1179 ( + "Tue, 14 Dec 2021 10:48:25 +0000" "HTML test" ( + ( + "Name" NIL "email" "example.com" + ) + ) ( + ( + "Name" NIL "email" "example.com" + ) + ) ( + ( + "Name" NIL "email" "example.com" + ) + ) ( + ( + "email@example.com" NIL "email" "example.com" + ) + ) NIL NIL NIL "" + ) ( + ( + "text" "plain" ( + "charset" "utf-8" "format" "flowed" + ) NIL NIL "7bit" 30 0 "6891396510cbadf4e2cfe31aee5bd25f" NIL NIL NIL + ) ( + "text" "html" ( + "charset" "utf-8" + ) NIL NIL "7bit" 173 8 "1a04bd2ec90f44a42792eacdc14fe8ea" NIL NIL NIL + ) "alternative" ( + "boundary" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ) NIL "en-US" NIL + ) 0 "eb1ac6e049a544c2bda1b7787e03db50" ( + "attachment" ( + "filename" "attached_email.eml" + ) + ) NIL NIL + ) "mixed" ( + "boundary" "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ) NIL NIL NIL +) + +BODY[] {1649} +Content-Type: multipart/mixed; + boundary=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + +--bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +This is a message with a base64 encoded attached email +--bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +Content-Disposition: attachment; filename="attached_email.eml" +Content-Type: message/rfc822 +Content-Transfer-Encoding: base64 + +VG86ICJlbWFpbEBleGFtcGxlLmNvbSIgPGVtYWlsQGV4YW1wbGUuY29tPg0KRnJvbTogTmFtZSA8 +ZW1haWxAZXhhbXBsZS5jb20+DQpTdWJqZWN0OiBIVE1MIHRlc3QNCk1lc3NhZ2UtSUQ6IDxyYW5k +b20tbWVzc2FnZS1pZEBleGFtcGxlLmNvbT4NCkRhdGU6IFR1ZSwgMTQgRGVjIDIwMjEgMTE6NDg6 +MjUgKzAxMDANCk1JTUUtVmVyc2lvbjogMS4wDQpDb250ZW50LVR5cGU6IG11bHRpcGFydC9hbHRl +cm5hdGl2ZTsNCiBib3VuZGFyeT0iYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYSINCkNvbnRlbnQtTGFuZ3VhZ2U6IGVuLVVTDQoNClRoaXMgaXMgYSBtdWx0aS1wYXJ0IG1l +c3NhZ2UgaW4gTUlNRSBmb3JtYXQuDQotLWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWENCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD11dGYtODsgZm9ybWF0 +PWZsb3dlZA0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogN2JpdA0KDQpUaGlzIGlzIGFuICpI +VE1MKiB0ZXN0IG1lc3NhZ2UNCi0tYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYQ0KQ29udGVudC1UeXBlOiB0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgNCkNvbnRlbnQtVHJh +bnNmZXItRW5jb2Rpbmc6IDdiaXQNCg0KPGh0bWw+DQogIDxoZWFkPg0KICAgIDxtZXRhIGh0dHAt +ZXF1aXY9ImNvbnRlbnQtdHlwZSIgY29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PVVURi04Ij4N +CiAgPC9oZWFkPg0KICA8Ym9keT4NCiAgICBUaGlzIGlzIGFuIDxiPkhUTUw8L2I+IHRlc3QgbWVz +c2FnZQ0KICA8L2JvZHk+DQo8L2h0bWw+DQoNCi0tYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWFhYWFhYS0tDQo= +--bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-- + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {83} +Content-Type: multipart/mixed; + boundary=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + + +---------------------------------- +BODY[TEXT] {1566} +--bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +This is a message with a base64 encoded attached email +--bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +Content-Disposition: attachment; filename="attached_email.eml" +Content-Type: message/rfc822 +Content-Transfer-Encoding: base64 + +VG86ICJlbWFpbEBleGFtcGxlLmNvbSIgPGVtYWlsQGV4YW1wbGUuY29tPg0KRnJvbTogTmFtZSA8 +ZW1haWxAZXhhbXBsZS5jb20+DQpTdWJqZWN0OiBIVE1MIHRlc3QNCk1lc3NhZ2UtSUQ6IDxyYW5k +b20tbWVzc2FnZS1pZEBleGFtcGxlLmNvbT4NCkRhdGU6IFR1ZSwgMTQgRGVjIDIwMjEgMTE6NDg6 +MjUgKzAxMDANCk1JTUUtVmVyc2lvbjogMS4wDQpDb250ZW50LVR5cGU6IG11bHRpcGFydC9hbHRl +cm5hdGl2ZTsNCiBib3VuZGFyeT0iYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYSINCkNvbnRlbnQtTGFuZ3VhZ2U6IGVuLVVTDQoNClRoaXMgaXMgYSBtdWx0aS1wYXJ0IG1l +c3NhZ2UgaW4gTUlNRSBmb3JtYXQuDQotLWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWENCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD11dGYtODsgZm9ybWF0 +PWZsb3dlZA0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogN2JpdA0KDQpUaGlzIGlzIGFuICpI +VE1MKiB0ZXN0IG1lc3NhZ2UNCi0tYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYQ0KQ29udGVudC1UeXBlOiB0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgNCkNvbnRlbnQtVHJh +bnNmZXItRW5jb2Rpbmc6IDdiaXQNCg0KPGh0bWw+DQogIDxoZWFkPg0KICAgIDxtZXRhIGh0dHAt +ZXF1aXY9ImNvbnRlbnQtdHlwZSIgY29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PVVURi04Ij4N +CiAgPC9oZWFkPg0KICA8Ym9keT4NCiAgICBUaGlzIGlzIGFuIDxiPkhUTUw8L2I+IHRlc3QgbWVz +c2FnZQ0KICA8L2JvZHk+DQo8L2h0bWw+DQoNCi0tYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWFhYWFhYS0tDQo= +--bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-- + +---------------------------------- +BODY[MIME] {84} +Content-Type: multipart/mixed; + boundary=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + + +---------------------------------- +BODY[1] {54} +This is a message with a base64 encoded attached email +BINARY[1] {54} +This is a message with a base64 encoded attached email +BINARY.SIZE[1] 54 +---------------------------------- +BODY[1.HEADER] {73} +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[1.TEXT] {54} +This is a message with a base64 encoded attached email +---------------------------------- +BODY[1.MIME] {74} +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[2] {1179} +VG86ICJlbWFpbEBleGFtcGxlLmNvbSIgPGVtYWlsQGV4YW1wbGUuY29tPg0KRnJvbTogTmFtZSA8 +ZW1haWxAZXhhbXBsZS5jb20+DQpTdWJqZWN0OiBIVE1MIHRlc3QNCk1lc3NhZ2UtSUQ6IDxyYW5k +b20tbWVzc2FnZS1pZEBleGFtcGxlLmNvbT4NCkRhdGU6IFR1ZSwgMTQgRGVjIDIwMjEgMTE6NDg6 +MjUgKzAxMDANCk1JTUUtVmVyc2lvbjogMS4wDQpDb250ZW50LVR5cGU6IG11bHRpcGFydC9hbHRl +cm5hdGl2ZTsNCiBib3VuZGFyeT0iYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYSINCkNvbnRlbnQtTGFuZ3VhZ2U6IGVuLVVTDQoNClRoaXMgaXMgYSBtdWx0aS1wYXJ0IG1l +c3NhZ2UgaW4gTUlNRSBmb3JtYXQuDQotLWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWENCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD11dGYtODsgZm9ybWF0 +PWZsb3dlZA0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogN2JpdA0KDQpUaGlzIGlzIGFuICpI +VE1MKiB0ZXN0IG1lc3NhZ2UNCi0tYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYQ0KQ29udGVudC1UeXBlOiB0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgNCkNvbnRlbnQtVHJh +bnNmZXItRW5jb2Rpbmc6IDdiaXQNCg0KPGh0bWw+DQogIDxoZWFkPg0KICAgIDxtZXRhIGh0dHAt +ZXF1aXY9ImNvbnRlbnQtdHlwZSIgY29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PVVURi04Ij4N +CiAgPC9oZWFkPg0KICA8Ym9keT4NCiAgICBUaGlzIGlzIGFuIDxiPkhUTUw8L2I+IHRlc3QgbWVz +c2FnZQ0KICA8L2JvZHk+DQo8L2h0bWw+DQoNCi0tYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWFhYWFhYS0tDQo= +BINARY[2] {16} +[binary content] +BINARY.SIZE[2] 872 +---------------------------------- +BODY[2.HEADER] {319} +To: "email@example.com" +From: Name +Subject: HTML test +Message-ID: +Date: Tue, 14 Dec 2021 11:48:25 +0100 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +Content-Language: en-US + + +---------------------------------- +BODY[2.TEXT] {553} +This is a multi-part message in MIME format. +--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +Content-Type: text/plain; charset=utf-8; format=flowed +Content-Transfer-Encoding: 7bit + +This is an *HTML* test message +--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 7bit + + + + + + + This is an HTML test message + + + +--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-- + +---------------------------------- +BODY[2.MIME] {119} +Content-Type: multipart/alternative; + boundary="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +Content-Language: en-US + + +---------------------------------- +BODY[2.1] {30} +This is an *HTML* test message +BINARY[2.1] {30} +This is an *HTML* test message +BINARY.SIZE[2.1] 30 +---------------------------------- +BODY[2.1.HEADER] {91} +Content-Type: text/plain; charset=utf-8; format=flowed +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[2.1.TEXT] {30} +This is an *HTML* test message +---------------------------------- +BODY[2.1.MIME] {91} +Content-Type: text/plain; charset=utf-8; format=flowed +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[2.2] {173} + + + + + + This is an HTML test message + + + +BINARY[2.2] {173} + + + + + + This is an HTML test message + + + +BINARY.SIZE[2.2] 173 +---------------------------------- +BODY[2.2.HEADER] {75} +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[2.2.TEXT] {173} + + + + + + This is an HTML test message + + + +---------------------------------- +BODY[2.2.MIME] {75} +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {2} + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {0} + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)] {84} +Content-Type: multipart/mixed; + boundary=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +pe: multipart/mixed; + bou +---------------------------------- diff --git a/tests/resources/imap/008.txt b/tests/resources/imap/008.txt new file mode 100644 index 00000000..c35e1c3d --- /dev/null +++ b/tests/resources/imap/008.txt @@ -0,0 +1,30 @@ +Content-Type: multipart/mixed; + boundary=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + +--bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +This is a message with a base64 encoded attached email +--bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +Content-Disposition: attachment; filename="attached_email.eml" +Content-Type: message/rfc822 +Content-Transfer-Encoding: base64 + +VG86ICJlbWFpbEBleGFtcGxlLmNvbSIgPGVtYWlsQGV4YW1wbGUuY29tPg0KRnJvbTogTmFtZSA8 +ZW1haWxAZXhhbXBsZS5jb20+DQpTdWJqZWN0OiBIVE1MIHRlc3QNCk1lc3NhZ2UtSUQ6IDxyYW5k +b20tbWVzc2FnZS1pZEBleGFtcGxlLmNvbT4NCkRhdGU6IFR1ZSwgMTQgRGVjIDIwMjEgMTE6NDg6 +MjUgKzAxMDANCk1JTUUtVmVyc2lvbjogMS4wDQpDb250ZW50LVR5cGU6IG11bHRpcGFydC9hbHRl +cm5hdGl2ZTsNCiBib3VuZGFyeT0iYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYSINCkNvbnRlbnQtTGFuZ3VhZ2U6IGVuLVVTDQoNClRoaXMgaXMgYSBtdWx0aS1wYXJ0IG1l +c3NhZ2UgaW4gTUlNRSBmb3JtYXQuDQotLWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWENCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD11dGYtODsgZm9ybWF0 +PWZsb3dlZA0KQ29udGVudC1UcmFuc2Zlci1FbmNvZGluZzogN2JpdA0KDQpUaGlzIGlzIGFuICpI +VE1MKiB0ZXN0IG1lc3NhZ2UNCi0tYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYQ0KQ29udGVudC1UeXBlOiB0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgNCkNvbnRlbnQtVHJh +bnNmZXItRW5jb2Rpbmc6IDdiaXQNCg0KPGh0bWw+DQogIDxoZWFkPg0KICAgIDxtZXRhIGh0dHAt +ZXF1aXY9ImNvbnRlbnQtdHlwZSIgY29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PVVURi04Ij4N +CiAgPC9oZWFkPg0KICA8Ym9keT4NCiAgICBUaGlzIGlzIGFuIDxiPkhUTUw8L2I+IHRlc3QgbWVz +c2FnZQ0KICA8L2JvZHk+DQo8L2h0bWw+DQoNCi0tYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh +YWFhYWFhYWFhYWFhYS0tDQo= +--bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-- diff --git a/tests/resources/imap/009.imap b/tests/resources/imap/009.imap new file mode 100644 index 00000000..d10232ad --- /dev/null +++ b/tests/resources/imap/009.imap @@ -0,0 +1,325 @@ +BODY ( + ( + "text" "html" ( + "charset" "us-ascii" + ) NIL NIL "base64" 239 3 + ) ( + "message" "rfc822" NIL NIL NIL NIL 723 ( + NIL "Exporting my book about coffee tables" ( + ( + "Cosmo Kramer" NIL "kramer" "kramerica.com" + ) + ) ( + ( + "Cosmo Kramer" NIL "kramer" "kramerica.com" + ) + ) ( + ( + "Cosmo Kramer" NIL "kramer" "kramerica.com" + ) + ) NIL NIL NIL NIL NIL + ) ( + ( + "text" "plain" ( + "charset" "utf-16" + ) NIL NIL "quoted-printable" 228 3 + ) ( + "image" "gif" ( + "name" "Book about ☕ tables.gif" + ) NIL NIL "Base64" 56 + ) "mixed" + ) 0 + ) "mixed" +) + +BODYSTRUCTURE ( + ( + "text" "html" ( + "charset" "us-ascii" + ) NIL NIL "base64" 239 3 "07aab44e51c5f1833a5d19f2e1804c4b" NIL NIL NIL + ) ( + "message" "rfc822" NIL NIL NIL NIL 723 ( + NIL "Exporting my book about coffee tables" ( + ( + "Cosmo Kramer" NIL "kramer" "kramerica.com" + ) + ) ( + ( + "Cosmo Kramer" NIL "kramer" "kramerica.com" + ) + ) ( + ( + "Cosmo Kramer" NIL "kramer" "kramerica.com" + ) + ) NIL NIL NIL NIL NIL + ) ( + ( + "text" "plain" ( + "charset" "utf-16" + ) NIL NIL "quoted-printable" 228 3 "3a942a99cdd8a099ae107d3867ec20fb" NIL NIL NIL + ) ( + "image" "gif" ( + "name" "Book about ☕ tables.gif" + ) NIL NIL "Base64" 56 "d40fa7f401e9dc2df56cbb740d65ff52" ( + "attachment" ( + + ) + ) NIL NIL + ) "mixed" ( + "boundary" "giddyup" + ) NIL NIL NIL + ) 0 "cdb0382a03a15601fb1b3c7422521620" NIL NIL NIL + ) "mixed" ( + "boundary" "festivus" + ) NIL NIL NIL +) + +BODY[] {1457} +From: Art Vandelay (Vandelay Industries) +To: "Colleagues": "James Smythe" ; Friends: + jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= ; +Date: Sat, 20 Nov 2021 14:22:01 -0800 +Subject: Why not both importing AND exporting? =?utf-8?b?4pi6?= +Content-Type: multipart/mixed; boundary="festivus"; + +--festivus +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: base64 + +PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle +HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm +cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8 +gJiN4MjYzQTs8L3A+PC9odG1sPg== +--festivus +Content-Type: message/rfc822 + +From: "Cosmo Kramer" +Subject: Exporting my book about coffee tables +Content-Type: multipart/mixed; boundary="giddyup"; + +--giddyup +Content-Type: text/plain; charset="utf-16" +Content-Transfer-Encoding: quoted-printable + +=FF=FE=0C!5=D8"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8"=DD =005=D8"= +=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD = +=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8"= +=DD5=D8=1E=DD5=D80=DD5=D8"=DD!=00 +--giddyup +Content-Type: image/gif; name*1="about "; name*0="Book "; + name*2*=utf-8''%e2%98%95 tables.gif +Content-Transfer-Encoding: Base64 +Content-Disposition: attachment + +R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 +--giddyup-- +--festivus-- + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {349} +From: Art Vandelay (Vandelay Industries) +To: "Colleagues": "James Smythe" ; Friends: + jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= ; +Date: Sat, 20 Nov 2021 14:22:01 -0800 +Subject: Why not both importing AND exporting? =?utf-8?b?4pi6?= +Content-Type: multipart/mixed; boundary="festivus"; + + +---------------------------------- +BODY[TEXT] {1108} +--festivus +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: base64 + +PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle +HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm +cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8 +gJiN4MjYzQTs8L3A+PC9odG1sPg== +--festivus +Content-Type: message/rfc822 + +From: "Cosmo Kramer" +Subject: Exporting my book about coffee tables +Content-Type: multipart/mixed; boundary="giddyup"; + +--giddyup +Content-Type: text/plain; charset="utf-16" +Content-Transfer-Encoding: quoted-printable + +=FF=FE=0C!5=D8"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8"=DD =005=D8"= +=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD = +=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8"= +=DD5=D8=1E=DD5=D80=DD5=D8"=DD!=00 +--giddyup +Content-Type: image/gif; name*1="about "; name*0="Book "; + name*2*=utf-8''%e2%98%95 tables.gif +Content-Transfer-Encoding: Base64 +Content-Disposition: attachment + +R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 +--giddyup-- +--festivus-- + +---------------------------------- +BODY[MIME] {54} +Content-Type: multipart/mixed; boundary="festivus"; + + +---------------------------------- +BODY[1] {239} +PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle +HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm +cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8 +gJiN4MjYzQTs8L3A+PC9odG1sPg== +BINARY[1] {175} +

I was thinking about quitting the “exporting” to focus just on the “importing”,

but then I thought, why not do both? ☺

+BINARY.SIZE[1] 175 +---------------------------------- +BODY[1.HEADER] {79} +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: base64 + + +---------------------------------- +BODY[1.TEXT] {239} +PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle +HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm +cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8 +gJiN4MjYzQTs8L3A+PC9odG1sPg== +---------------------------------- +BODY[1.MIME] {80} +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: base64 + + +---------------------------------- +BODY[2] {723} +From: "Cosmo Kramer" +Subject: Exporting my book about coffee tables +Content-Type: multipart/mixed; boundary="giddyup"; + +--giddyup +Content-Type: text/plain; charset="utf-16" +Content-Transfer-Encoding: quoted-printable + +=FF=FE=0C!5=D8"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8"=DD =005=D8"= +=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD = +=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8"= +=DD5=D8=1E=DD5=D80=DD5=D8"=DD!=00 +--giddyup +Content-Type: image/gif; name*1="about "; name*0="Book "; + name*2*=utf-8''%e2%98%95 tables.gif +Content-Transfer-Encoding: Base64 +Content-Disposition: attachment + +R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 +--giddyup-- +BINARY[2] {16} +[binary content] +BINARY.SIZE[2] 723 +---------------------------------- +BODY[2.HEADER] {143} +From: "Cosmo Kramer" +Subject: Exporting my book about coffee tables +Content-Type: multipart/mixed; boundary="giddyup"; + + +---------------------------------- +BODY[2.TEXT] {580} +--giddyup +Content-Type: text/plain; charset="utf-16" +Content-Transfer-Encoding: quoted-printable + +=FF=FE=0C!5=D8"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8"=DD =005=D8"= +=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD = +=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8"= +=DD5=D8=1E=DD5=D80=DD5=D8"=DD!=00 +--giddyup +Content-Type: image/gif; name*1="about "; name*0="Book "; + name*2*=utf-8''%e2%98%95 tables.gif +Content-Transfer-Encoding: Base64 +Content-Disposition: attachment + +R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 +--giddyup-- +---------------------------------- +BODY[2.MIME] {53} +Content-Type: multipart/mixed; boundary="giddyup"; + + +---------------------------------- +BODY[2.1] {228} +=FF=FE=0C!5=D8"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8"=DD =005=D8"= +=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD = +=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8"= +=DD5=D8=1E=DD5=D80=DD5=D8"=DD!=00 +BINARY[2.1] {101} +ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨 𝔭𝔩𝔢𝔞𝔰𝔢! +BINARY.SIZE[2.1] 101 +---------------------------------- +BODY[2.1.HEADER] {88} +Content-Type: text/plain; charset="utf-16" +Content-Transfer-Encoding: quoted-printable + + +---------------------------------- +BODY[2.1.TEXT] {228} +=FF=FE=0C!5=D8"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8"=DD =005=D8"= +=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD = +=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8"= +=DD5=D8=1E=DD5=D80=DD5=D8"=DD!=00 +---------------------------------- +BODY[2.1.MIME] {89} +Content-Type: text/plain; charset="utf-16" +Content-Transfer-Encoding: quoted-printable + + +---------------------------------- +BODY[2.2] {56} +R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 +BINARY[2.2] {16} +[binary content] +BINARY.SIZE[2.2] 42 +---------------------------------- +BODY[2.2.HEADER] {175} +Content-Type: image/gif; name*1="about "; name*0="Book "; + name*2*=utf-8''%e2%98%95 tables.gif +Content-Transfer-Encoding: Base64 +Content-Disposition: attachment + + +---------------------------------- +BODY[2.2.TEXT] {56} +R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 +---------------------------------- +BODY[2.2.MIME] {176} +Content-Type: image/gif; name*1="about "; name*0="Book "; + name*2*=utf-8''%e2%98%95 tables.gif +Content-Transfer-Encoding: Base64 +Content-Disposition: attachment + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {196} +From: Art Vandelay (Vandelay Industries) +To: "Colleagues": "James Smythe" ; Friends: + jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= ; + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {25} +Vandelay (Vandelay Industries) +To: "Colleagues": "James Smythe" ; Friends: + jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= ; +Date: Sat, 20 Nov 2021 14:22:01 -0800 +Content-Type: multipart/mixed; boundary="festivus"; + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +Vandelay (Vandelay Industries) +To: "Colleagues": "James Smythe" ; Friends: + jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= ; +Date: Sat, 20 Nov 2021 14:22:01 -0800 +Subject: Why not both importing AND exporting? =?utf-8?b?4pi6?= +Content-Type: multipart/mixed; boundary="festivus"; + +--festivus +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: base64 + +PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle +HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm +cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8 +gJiN4MjYzQTs8L3A+PC9odG1sPg== +--festivus +Content-Type: message/rfc822 + +From: "Cosmo Kramer" +Subject: Exporting my book about coffee tables +Content-Type: multipart/mixed; boundary="giddyup"; + +--giddyup +Content-Type: text/plain; charset="utf-16" +Content-Transfer-Encoding: quoted-printable + +=FF=FE=0C!5=D8"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8"=DD =005=D8"= +=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD = +=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8"= +=DD5=D8=1E=DD5=D80=DD5=D8"=DD!=00 +--giddyup +Content-Type: image/gif; name*1="about "; name*0="Book "; + name*2*=utf-8''%e2%98%95 tables.gif +Content-Transfer-Encoding: Base64 +Content-Disposition: attachment + +R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 +--giddyup-- +--festivus-- diff --git a/tests/resources/imap/010.imap b/tests/resources/imap/010.imap new file mode 100644 index 00000000..e62beaba --- /dev/null +++ b/tests/resources/imap/010.imap @@ -0,0 +1,126 @@ +BODY ( + "message" "rfc822" NIL NIL NIL NIL 88 ( + "Sun, 12 Aug 2012 09:34:56 +0000" "submsg" ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 12 1 + ) 0 +) + +BODYSTRUCTURE ( + "message" "rfc822" NIL NIL NIL NIL 88 ( + "Sun, 12 Aug 2012 09:34:56 +0000" "submsg" ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 12 1 "f0ef7081e1539ac00ef5b761b4fb01b3" NIL NIL NIL + ) 0 "dfea5a78f321b331e6d7983a1f9cc6b7" NIL NIL NIL +) + +BODY[] {196} +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +BINARY[] {16} +[binary content] +BINARY.SIZE[] 88 +---------------------------------- +BODY[HEADER] {108} +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + + +---------------------------------- +BODY[TEXT] {88} +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +---------------------------------- +BODY[MIME] {31} +Content-Type: message/rfc822 + + +---------------------------------- +BODY[1] {88} +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world + +---------------------------------- +BODY[1.HEADER] {76} +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + + +---------------------------------- +BODY[1.TEXT] {12} +Hello world + +---------------------------------- +BODY[1.MIME] {2} + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {24} +From: user@domain.org + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {14} +@domain.org + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)] {109} +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +MIME-Version: 1.0 +Content-Type: message/rfc822 + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +@domain.org +Date: Sat, 24 +---------------------------------- diff --git a/tests/resources/imap/010.txt b/tests/resources/imap/010.txt new file mode 100644 index 00000000..a5cc4bde --- /dev/null +++ b/tests/resources/imap/010.txt @@ -0,0 +1,10 @@ +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg + +Hello world diff --git a/tests/resources/imap/011.imap b/tests/resources/imap/011.imap new file mode 100644 index 00000000..68334f7b --- /dev/null +++ b/tests/resources/imap/011.imap @@ -0,0 +1,324 @@ +BODY ( + "message" "rfc822" NIL NIL NIL NIL 271 ( + "Sun, 12 Aug 2012 09:34:56 +0000" "submsg" ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) NIL NIL NIL NIL NIL + ) ( + ( + "message" NIL NIL NIL NIL NIL 42 ( + NIL "m1" ( + ( + NIL NIL "m1" "example.com" + ) + ) ( + ( + NIL NIL "m1" "example.com" + ) + ) ( + ( + NIL NIL "m1" "example.com" + ) + ) NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 8 1 + ) 0 + ) ( + "message" NIL NIL NIL NIL NIL 42 ( + NIL "m2" ( + ( + NIL NIL "m2" "example.com" + ) + ) ( + ( + NIL NIL "m2" "example.com" + ) + ) ( + ( + NIL NIL "m2" "example.com" + ) + ) NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 8 1 + ) 0 + ) "digest" + ) 0 +) + +BODYSTRUCTURE ( + "message" "rfc822" NIL NIL NIL NIL 271 ( + "Sun, 12 Aug 2012 09:34:56 +0000" "submsg" ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) NIL NIL NIL NIL NIL + ) ( + ( + "message" NIL NIL NIL NIL NIL 42 ( + NIL "m1" ( + ( + NIL NIL "m1" "example.com" + ) + ) ( + ( + NIL NIL "m1" "example.com" + ) + ) ( + ( + NIL NIL "m1" "example.com" + ) + ) NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 8 1 "8dc313ad8cf1d82dbe8d46f5f0d3d79c" NIL NIL NIL + ) 0 "702907ad1c165219425153a8f0a5f578" NIL NIL NIL + ) ( + "message" NIL NIL NIL NIL NIL 42 ( + NIL "m2" ( + ( + NIL NIL "m2" "example.com" + ) + ) ( + ( + NIL NIL "m2" "example.com" + ) + ) ( + ( + NIL NIL "m2" "example.com" + ) + ) NIL NIL NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 8 1 "f344a10ee7adfdcfc29650b6e31601d8" NIL NIL NIL + ) 0 "0c79449f982ccecbc258d902cd989f69" NIL NIL NIL + ) "digest" ( + "boundary" "foo" + ) NIL NIL NIL + ) 0 "4935800d6cfad87d931093820097206a" NIL NIL NIL +) + +BODY[] {379} +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/digest; boundary="foo" + +prologue + +--foo + +From: m1@example.com +Subject: m1 + +m1 body + +--foo +X-Mime: m2 header + +From: m2@example.com +Subject: m2 + +m2 body + +--foo-- + +epilogue + +BINARY[] {16} +[binary content] +BINARY.SIZE[] 260 +---------------------------------- +BODY[HEADER] {108} +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + + +---------------------------------- +BODY[TEXT] {271} +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/digest; boundary="foo" + +prologue + +--foo + +From: m1@example.com +Subject: m1 + +m1 body + +--foo +X-Mime: m2 header + +From: m2@example.com +Subject: m2 + +m2 body + +--foo-- + +epilogue + +---------------------------------- +BODY[MIME] {31} +Content-Type: message/rfc822 + + +---------------------------------- +BODY[1] {271} +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/digest; boundary="foo" + +prologue + +--foo + +From: m1@example.com +Subject: m1 + +m1 body + +--foo +X-Mime: m2 header + +From: m2@example.com +Subject: m2 + +m2 body + +--foo-- + +epilogue + +---------------------------------- +BODY[1.HEADER] {123} +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/digest; boundary="foo" + + +---------------------------------- +BODY[1.TEXT] {137} +prologue + +--foo + +From: m1@example.com +Subject: m1 + +m1 body + +--foo +X-Mime: m2 header + +From: m2@example.com +Subject: m2 + +m2 body + +--foo-- +---------------------------------- +BODY[1.MIME] {49} +Content-Type: multipart/digest; boundary="foo" + + +---------------------------------- +BODY[1.1] {42} +From: m1@example.com +Subject: m1 + +m1 body + +---------------------------------- +BODY[1.1.HEADER] {34} +From: m1@example.com +Subject: m1 + + +---------------------------------- +BODY[1.1.TEXT] {8} +m1 body + +---------------------------------- +BODY[1.1.MIME] {2} + + +---------------------------------- +BODY[1.2] {42} +From: m2@example.com +Subject: m2 + +m2 body + +---------------------------------- +BODY[1.2.HEADER] {34} +From: m2@example.com +Subject: m2 + + +---------------------------------- +BODY[1.2.TEXT] {8} +m2 body + +---------------------------------- +BODY[1.2.MIME] {2} + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {24} +From: user@domain.org + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {14} +@domain.org + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)] {109} +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +MIME-Version: 1.0 +Content-Type: message/rfc822 + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +@domain.org +Date: Sat, 24 +---------------------------------- diff --git a/tests/resources/imap/011.txt b/tests/resources/imap/011.txt new file mode 100644 index 00000000..1691395f --- /dev/null +++ b/tests/resources/imap/011.txt @@ -0,0 +1,30 @@ +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/digest; boundary="foo" + +prologue + +--foo + +From: m1@example.com +Subject: m1 + +m1 body + +--foo +X-Mime: m2 header + +From: m2@example.com +Subject: m2 + +m2 body + +--foo-- + +epilogue diff --git a/tests/resources/imap/012.imap b/tests/resources/imap/012.imap new file mode 100644 index 00000000..e42f026e --- /dev/null +++ b/tests/resources/imap/012.imap @@ -0,0 +1,304 @@ +BODY ( + ( + "text" "x-myown" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 6 1 + ) ( + "message" "rfc822" NIL NIL NIL NIL 280 ( + "Sun, 12 Aug 2012 09:34:56 +0000" "submsg" ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) NIL NIL NIL NIL NIL + ) ( + ( + "text" "html" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 19 1 + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 20 1 + ) "alternative" + ) 0 + ) "mixed" +) + +BODYSTRUCTURE ( + ( + "text" "x-myown" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 6 1 "b1946ac92492d2347c6235b4d2611184" NIL NIL NIL + ) ( + "message" "rfc822" NIL NIL NIL NIL 280 ( + "Sun, 12 Aug 2012 09:34:56 +0000" "submsg" ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) ( + ( + NIL NIL "sub" "domain.org" + ) + ) NIL NIL NIL NIL NIL + ) ( + ( + "text" "html" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 19 1 "35c5b687e359e8ce7be1f1ecafd9b475" NIL NIL NIL + ) ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 20 1 "deeff770fadb17c664d431b97bcd05c5" NIL NIL NIL + ) "alternative" ( + "boundary" "sub1" + ) NIL NIL NIL + ) 0 "735ed696bc05fdf6840de404781d5d77" NIL NIL NIL + ) "mixed" ( + "boundary" "foo bar" + ) NIL NIL NIL +) + +BODY[] {565} +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + +Root MIME prologue + +--foo bar +Content-Type: text/x-myown; charset=us-ascii + +hello + +--foo bar +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +--foo bar-- +Root MIME epilogue + +BINARY.SIZE[] 0 +---------------------------------- +BODY[HEADER] {130} +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + + +---------------------------------- +BODY[TEXT] {435} +Root MIME prologue + +--foo bar +Content-Type: text/x-myown; charset=us-ascii + +hello + +--foo bar +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +--foo bar-- +Root MIME epilogue + +---------------------------------- +BODY[MIME] {53} +Content-Type: multipart/mixed; boundary="foo + bar" + + +---------------------------------- +BODY[1] {6} +hello + +BINARY[1] {6} +hello + +BINARY.SIZE[1] 6 +---------------------------------- +BODY[1.HEADER] {46} +Content-Type: text/x-myown; charset=us-ascii + + +---------------------------------- +BODY[1.TEXT] {6} +hello + +---------------------------------- +BODY[1.MIME] {47} +Content-Type: text/x-myown; charset=us-ascii + + +---------------------------------- +BODY[2] {280} +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +BINARY[2] {16} +[binary content] +BINARY.SIZE[2] 280 +---------------------------------- +BODY[2.HEADER] {129} +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + + +---------------------------------- +BODY[2.TEXT] {151} +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +---------------------------------- +BODY[2.MIME] {55} +Content-Type: multipart/alternative; boundary="sub1" + + +---------------------------------- +BODY[2.1] {19} +

Hello world

+ +BINARY[2.1] {19} +

Hello world

+ +BINARY.SIZE[2.1] 19 +---------------------------------- +BODY[2.1.HEADER] {25} +Content-Type: text/html + + +---------------------------------- +BODY[2.1.TEXT] {19} +

Hello world

+ +---------------------------------- +BODY[2.1.MIME] {26} +Content-Type: text/html + + +---------------------------------- +BODY[2.2] {20} +Hello another world + +BINARY[2.2] {20} +Hello another world + +BINARY.SIZE[2.2] 20 +---------------------------------- +BODY[2.2.HEADER] {26} +Content-Type: text/plain + + +---------------------------------- +BODY[2.2.TEXT] {20} +Hello another world + +---------------------------------- +BODY[2.2.MIME] {27} +Content-Type: text/plain + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {24} +From: user@domain.org + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {14} +@domain.org + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)] {131} +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} +@domain.org +Date: Sat, 24 +---------------------------------- diff --git a/tests/resources/imap/012.txt b/tests/resources/imap/012.txt new file mode 100644 index 00000000..15f1fd3c --- /dev/null +++ b/tests/resources/imap/012.txt @@ -0,0 +1,37 @@ +From: user@domain.org +Date: Sat, 24 Mar 2007 23:00:00 +0200 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="foo + bar" + +Root MIME prologue + +--foo bar +Content-Type: text/x-myown; charset=us-ascii + +hello + +--foo bar +Content-Type: message/rfc822 + +From: sub@domain.org +Date: Sun, 12 Aug 2012 12:34:56 +0300 +Subject: submsg +Content-Type: multipart/alternative; boundary="sub1" + +Sub MIME prologue +--sub1 +Content-Type: text/html + +

Hello world

+ +--sub1 +Content-Type: text/plain + +Hello another world + +--sub1-- +Sub MIME epilogue + +--foo bar-- +Root MIME epilogue diff --git a/tests/resources/imap/013.imap b/tests/resources/imap/013.imap new file mode 100644 index 00000000..0dea734a --- /dev/null +++ b/tests/resources/imap/013.imap @@ -0,0 +1,126 @@ +BODY ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 356 13 +) + +BODYSTRUCTURE ( + "text" "plain" ( + "charset" "us-ascii" + ) NIL NIL "7bit" 356 13 "77beb490b61fa7ed17f997c4124a57bf" NIL NIL NIL +) + +BODY[] {697} +Date: Mon, 13 Aug 1998 17:42:41 +1000 +Message-Id: <199804130742.RAA20366@mai1host.whitehouse.gov> +From: Bill Clinton +To: A1 (The Enforcer) Gore +Subject: Map of Argentina with Description +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill + +BINARY[] {356} +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill + +BINARY.SIZE[] 356 +---------------------------------- +BODY[HEADER] {341} +Date: Mon, 13 Aug 1998 17:42:41 +1000 +Message-Id: <199804130742.RAA20366@mai1host.whitehouse.gov> +From: Bill Clinton +To: A1 (The Enforcer) Gore +Subject: Map of Argentina with Description +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[TEXT] {356} +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill + +---------------------------------- +BODY[MIME] {77} +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[1] {356} +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)] {107} +From: Bill Clinton +To: A1 (The Enforcer) Gore + + +---------------------------------- +BODY[HEADER.FIELDS (FROM TO)]<10> {25} + Clinton +From: Bill Clinton +To: A1 (The Enforcer) Gore +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +---------------------------------- +BODY[HEADER.FIELDS.NOT (SUBJECT CC)]<10> {25} + 13 Aug 1998 17:42:41 +10 +---------------------------------- diff --git a/tests/resources/imap/013.txt b/tests/resources/imap/013.txt new file mode 100644 index 00000000..4fcdd562 --- /dev/null +++ b/tests/resources/imap/013.txt @@ -0,0 +1,22 @@ +Date: Mon, 13 Aug 1998 17:42:41 +1000 +Message-Id: <199804130742.RAA20366@mai1host.whitehouse.gov> +From: Bill Clinton +To: A1 (The Enforcer) Gore +Subject: Map of Argentina with Description +MIME-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Hi A1, + +I finally figured out this MIME thing. Pretty cool. I'll send you +some sax music in .au files next week! + +Anyway, the attached image is really too small to get a good look at +Argentina. Try this for a much better map: + + http://www.1one1yp1anet.com/dest/sam/graphics/map-arg.htm + +Then again, shouldn't the CIA have something like that? + +Bill diff --git a/tests/src/imap/acl.rs b/tests/src/imap/acl.rs new file mode 100644 index 00000000..b5a10823 --- /dev/null +++ b/tests/src/imap/acl.rs @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::ResponseType; + +use super::{append::assert_append_message, AssertResult, ImapConnection, Type}; + +pub async fn test(mut imap_john: &mut ImapConnection, _imap_check: &mut ImapConnection) { + // Connect to all test accounts + let mut imap_jane = ImapConnection::connect(b"_w ").await; + let mut imap_bill = ImapConnection::connect(b"_z ").await; + for (imap, secret) in [ + (&mut imap_jane, "AGphbmUuc21pdGhAZXhhbXBsZS5jb20Ac2VjcmV0"), + (&mut imap_bill, "AGZvb2JhckBleGFtcGxlLmNvbQBzZWNyZXQ="), + ] { + imap.assert_read(Type::Untagged, ResponseType::Ok).await; + imap.send(&format!( + "AUTHENTICATE PLAIN {{{}+}}\r\n{}", + secret.len(), + secret + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + } + + // John should have no shared folders + imap_john.send("LIST \"\" \"*\"").await; + imap_john + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("Shared Folders", 0); + imap_john.send("NAMESPACE").await; + imap_john.assert_read(Type::Tagged, ResponseType::Ok).await; + + // List rights + imap_jane.send("LISTRIGHTS INBOX jdoe@example.com").await; + imap_jane + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* LISTRIGHTS \"INBOX\" \"jdoe@example.com\" r l ws i et k x p a"); + + // Jane shares her Inbox to John, expect a Shared Folders item in John's list + imap_jane.send("SETACL INBOX jdoe@example.com lr").await; + imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_john.send("LIST \"\" \"*\"").await; + imap_john + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* LIST (\\NoSelect) \"/\" \"Shared Folders\"") + .assert_equals("* LIST (\\NoSelect) \"/\" \"Shared Folders/Jane Smith\"") + .assert_equals("* LIST () \"/\" \"Shared Folders/Jane Smith/Inbox\""); + + // Grant access to Bill and check ACLs + imap_jane.send("GETACL INBOX").await; + imap_jane + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("\"jdoe@example.com\" rl"); + + imap_jane + .send("SETACL INBOX foobar@example.com lrxtws") + .await; + imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap_jane.send("GETACL INBOX").await; + imap_jane + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("\"jdoe@example.com\" rl") + .assert_contains("\"foobar@example.com\" tewsrxl"); + + imap_bill.send("LIST \"\" \"*\"").await; + imap_bill + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("Shared Folders/Jane Smith/Inbox"); + + // Namespace should now return the Shared Folders namespace + imap_john.send("NAMESPACE").await; + imap_john + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* NAMESPACE ((\"\" \"/\")) ((\"Shared Folders\" \"/\")) NIL"); + + // List John's right on Jane's Inbox + imap_john + .send("MYRIGHTS \"Shared Folders/Jane Smith/Inbox\"") + .await; + imap_john + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* MYRIGHTS \"Shared Folders/Jane Smith/Inbox\" rl"); + + // John should not be able to append messages + assert_append_message( + imap_john, + "Shared Folders/Jane Smith/Inbox", + "From: john\n\ncontents", + ResponseType::No, + ) + .await; + + // Grant insert access to John on Jane's Inbox, and try inserting the + // message again. + imap_jane.send("SETACL INBOX jdoe@example.com +i").await; + imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_john + .send("MYRIGHTS \"Shared Folders/Jane Smith/Inbox\"") + .await; + imap_john + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* MYRIGHTS \"Shared Folders/Jane Smith/Inbox\" rli"); + assert_append_message( + imap_john, + "Shared Folders/Jane Smith/Inbox", + "From: john\n\ncontents", + ResponseType::Ok, + ) + .await; + + // Only Bill should be allowed to delete messages on Jane's Inbox + for imap in [&mut imap_john, &mut imap_bill] { + imap.send("SELECT \"Shared Folders/Jane Smith/Inbox\"") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + } + imap_john.send("UID STORE 1 +FLAGS (\\Deleted)").await; + imap_john.assert_read(Type::Tagged, ResponseType::No).await; + + imap_bill.send("UID STORE 1 +FLAGS (\\Deleted)").await; + imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap_john.send("UID EXPUNGE").await; + imap_john.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap_john.send("UID FETCH 1 (PREVIEW)").await; + imap_john + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("contents"); + + imap_bill.send("UID EXPUNGE").await; + imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap_bill.send("UID FETCH 1 (PREVIEW)").await; + imap_bill + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("contents", 0); + + imap_bill + .send("STATUS \"Shared Folders/Jane Smith/Inbox\" (MESSAGES)") + .await; + imap_bill + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("(MESSAGES 0)"); + + // Test copying and moving between shared mailboxes + let uid = assert_append_message( + imap_john, + "INBOX", + "From: john\n\ncopy test", + ResponseType::Ok, + ) + .await + .into_append_uid(); + + imap_john.send("SELECT INBOX").await; + imap_john.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Copy from John's Inbox to Jane's Inbox + imap_john + .send(&format!( + "UID COPY {} \"Shared Folders/Jane Smith/Inbox\"", + uid + )) + .await; + let uid = imap_john + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_copy_uid(); + + // Check that both Bill and Jane can see the message + imap_bill.send("NOOP").await; + imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap_bill + .send(&format!("UID FETCH {} (PREVIEW)", uid)) + .await; + imap_bill + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("copy test"); + + imap_jane.send("SELECT INBOX").await; + imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap_jane + .send(&format!("UID FETCH {} (PREVIEW)", uid)) + .await; + imap_jane + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("copy test"); + + // Bill now moves the message to his own Inbox + imap_bill.send(&format!("UID MOVE {} INBOX", uid)).await; + let uid_moved = imap_bill + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_copy_uid(); + + // Both Jane and Bill should not see the message on Jane's Inbox anymore + imap_bill + .send(&format!("UID FETCH {} (PREVIEW)", uid)) + .await; + imap_bill + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("copy test", 0); + + imap_jane + .send(&format!("UID FETCH {} (PREVIEW)", uid)) + .await; + imap_jane + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("copy test", 0); + + // Check that the message has been moved to Bill's Inbox + imap_bill.send("SELECT INBOX").await; + imap_bill.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap_bill + .send(&format!("UID FETCH {} (PREVIEW)", uid_moved)) + .await; + imap_bill + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("copy test"); + + // Jane stops sharing with Bill, and removes Insert access to John + imap_jane.send("DELETEACL INBOX foobar@example.com").await; + imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap_jane.send("SETACL INBOX jdoe@example.com -i").await; + imap_jane.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap_jane.send("GETACL INBOX").await; + imap_jane + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("\"jdoe@example.com\" rl") + .assert_count("foobar@example.com", 0); + + // Bill should not have access to Jane's Inbox anymore + imap_bill.send("LIST \"\" \"*\"").await; + imap_bill + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("Shared Folders", 0); + + // And John should still have access + imap_john.send("LIST \"\" \"*\"").await; + imap_john + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("Shared Folders", 3); +} diff --git a/tests/src/imap/append.rs b/tests/src/imap/append.rs new file mode 100644 index 00000000..05550dac --- /dev/null +++ b/tests/src/imap/append.rs @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::{fs, io}; + +use imap_proto::ResponseType; + +use super::{resources_dir, AssertResult, ImapConnection, Type}; + +pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { + // Invalid APPEND commands + imap.send("APPEND \"All Mail\" {1+}\r\na").await; + imap.assert_read(Type::Tagged, ResponseType::No) + .await + .assert_response_code("CANNOT"); + imap.send("APPEND \"Does not exist\" {1+}\r\na").await; + imap.assert_read(Type::Tagged, ResponseType::No) + .await + .assert_response_code("TRYCREATE"); + + // Import test messages + let mut entries = fs::read_dir(&resources_dir()) + .unwrap() + .map(|res| res.map(|e| e.path())) + .collect::, io::Error>>() + .unwrap(); + + entries.sort(); + + let mut expected_uid = 1; + for file_name in entries.into_iter().take(20) { + if file_name.extension().map_or(true, |e| e != "txt") { + continue; + } + let raw_message = fs::read(&file_name).unwrap(); + + imap.send(&format!( + "APPEND INBOX (Flag_{}) {{{}}}", + file_name + .file_name() + .unwrap() + .to_str() + .unwrap() + .split_once('.') + .unwrap() + .0, + raw_message.len() + )) + .await; + imap.assert_read(Type::Continuation, ResponseType::Ok).await; + imap.send_untagged(std::str::from_utf8(&raw_message).unwrap()) + .await; + let result = imap + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_response_code(); + let mut code = result.split(' '); + assert_eq!(code.next(), Some("APPENDUID")); + assert_ne!(code.next(), Some("0")); + assert_eq!(code.next(), Some(expected_uid.to_string().as_str())); + expected_uid += 1; + } +} + +pub async fn assert_append_message( + imap: &mut ImapConnection, + folder: &str, + message: &str, + expected_response: ResponseType, +) -> Vec { + imap.send(&format!("APPEND \"{}\" {{{}}}", folder, message.len())) + .await; + imap.assert_read(Type::Continuation, ResponseType::Ok).await; + imap.send_untagged(message).await; + imap.assert_read(Type::Tagged, expected_response).await +} + +fn build_message(message: usize, in_reply_to: Option, thread_num: usize) -> String { + if let Some(in_reply_to) = in_reply_to { + format!( + "Message-ID: <{}@domain>\nReferences: <{}@domain>\nSubject: re: T{}\n\nreply\n", + message, in_reply_to, thread_num + ) + } else { + format!( + "Message-ID: <{}@domain>\nSubject: T{}\n\nmsg\n", + message, thread_num + ) + } +} + +pub fn build_messages() -> Vec { + let mut messages = Vec::new(); + for parent in 0..3 { + messages.push(build_message(parent, None, parent)); + for child in 0..3 { + messages.push(build_message( + ((parent + 1) * 10) + child, + parent.into(), + parent, + )); + } + } + messages +} diff --git a/tests/src/imap/basic.rs b/tests/src/imap/basic.rs new file mode 100644 index 00000000..0e7b02a6 --- /dev/null +++ b/tests/src/imap/basic.rs @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::ResponseType; + +use super::{AssertResult, ImapConnection, Type}; + +pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { + // Test CAPABILITY + imap.send("CAPABILITY").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Test NOOP + imap.send("NOOP").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Test ID + imap.send("ID").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* ID (\"name\" \"Stalwart IMAP\" \"version\" "); + + // Login should be disabled + imap.send("LOGIN jdoe@example.com secret").await; + imap.assert_read(Type::Tagged, ResponseType::No).await; + + // Try logging in with wrong password + imap.send("AUTHENTICATE PLAIN {24}").await; + imap.assert_read(Type::Continuation, ResponseType::Ok).await; + imap.send_untagged("AGJvYXR5AG1jYm9hdGZhY2U=").await; + imap.assert_read(Type::Tagged, ResponseType::No).await; +} diff --git a/tests/src/imap/body_structure.rs b/tests/src/imap/body_structure.rs index b1553956..1d789dd7 100644 --- a/tests/src/imap/body_structure.rs +++ b/tests/src/imap/body_structure.rs @@ -1,4 +1,4 @@ -use std::{fs, path::PathBuf}; +use std::fs; use imap::op::fetch::AsImapDataItem; use imap_proto::{ @@ -7,13 +7,11 @@ use imap_proto::{ }; use mail_parser::Message; +use super::resources_dir; + #[test] fn body_structure() { - let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - test_dir.push("resources"); - test_dir.push("imap"); - test_dir.push("messages"); - for file_name in fs::read_dir(&test_dir).unwrap() { + for file_name in fs::read_dir(&resources_dir()).unwrap() { let mut file_name = file_name.as_ref().unwrap().path(); if file_name.extension().map_or(true, |e| e != "txt") { continue; diff --git a/tests/src/imap/condstore.rs b/tests/src/imap/condstore.rs new file mode 100644 index 00000000..64d82fa4 --- /dev/null +++ b/tests/src/imap/condstore.rs @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::ResponseType; + +use crate::imap::{ + append::{assert_append_message, build_messages}, + AssertResult, +}; + +use super::{ImapConnection, Type}; + +pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) { + // Test CONDSTORE parameter + imap.send("SELECT INBOX (CONDSTORE)").await; + let hms = imap + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_highest_modseq(); + + // Unselect + imap.send("UNSELECT").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Create test folders + imap.send("CREATE Pecorino").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Enable CONDSTORE and QRESYNC + imap.send("ENABLE CONDSTORE QRESYNC").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Make sure modseq did not change after creating a mailbox + imap.send("SELECT Pecorino").await; + assert_eq!( + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_highest_modseq(), + hms + ); + imap_check.send("LIST \"\" \"*\"").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check.send("SELECT Pecorino (CONDSTORE)").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; + + // SEQ 0: Init + let mut messages = build_messages(); + let mut modseqs = vec![hms]; + + // SEQ 1: Append a message and make sure the modseq increased + assert_append_message(imap, "Pecorino", &messages.pop().unwrap(), ResponseType::Ok).await; + imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await; + modseqs.push( + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_highest_modseq(), + ); + assert_ne!(modseqs[modseqs.len() - 1], modseqs[modseqs.len() - 2]); + + // SEQ 2: Move out the message and make sure the modseq increased + imap.send("UID MOVE 1 \"Deleted Items\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* VANISHED 1"); + imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await; + modseqs.push( + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_highest_modseq(), + ); + assert_ne!(modseqs[modseqs.len() - 1], modseqs[modseqs.len() - 2]); + + // SEQ 3: Insert message + assert_append_message(imap, "Pecorino", &messages.pop().unwrap(), ResponseType::Ok).await; + imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await; + modseqs.push( + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_highest_modseq(), + ); + + // SEQ 4: Insert message + assert_append_message(imap, "Pecorino", &messages.pop().unwrap(), ResponseType::Ok).await; + imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await; + modseqs.push( + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_highest_modseq(), + ); + + // SEQ 5: Insert message + assert_append_message(imap, "Pecorino", &messages.pop().unwrap(), ResponseType::Ok).await; + imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await; + modseqs.push( + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_highest_modseq(), + ); + + // SEQ 6: Change a message flag + imap.send("UID STORE 4 +FLAGS.SILENT (\\Answered)").await; + modseqs.push( + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_modseq(), + ); + + // SEQ 7: Insert message + assert_append_message(imap, "Pecorino", &messages.pop().unwrap(), ResponseType::Ok).await; + imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await; + modseqs.push( + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_highest_modseq(), + ); + + // SEQ 8: Delete a message + imap.send("UID STORE 2 +FLAGS.SILENT (\\Deleted)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("EXPUNGE").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("VANISHED 2") + .assert_contains("* 3 EXISTS"); + imap.send("STATUS Pecorino (HIGHESTMODSEQ)").await; + modseqs.push( + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_highest_modseq(), + ); + + // Fetch changes since SEQ 0 + imap.send(&format!( + "UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)", + modseqs[0] + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("FETCH (", 3) + .assert_count("VANISHED", 0); + + // Fetch changes since SEQ 1, UID MOVE should count as a deletion + imap.send(&format!( + "UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)", + modseqs[1] + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("VANISHED", 1) + .assert_contains("VANISHED (EARLIER) 1") + .assert_count("FETCH (", 3); + + // Fetch changes since SEQ 3 + imap.send(&format!( + "UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)", + modseqs[3] + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("VANISHED", 1) + .assert_contains("VANISHED (EARLIER) 2") + .assert_count("FETCH (", 3); + + // Fetch changes since SEQ 4 + imap.send(&format!( + "UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)", + modseqs[4] + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("VANISHED", 1) + .assert_contains("VANISHED (EARLIER) 2") + .assert_count("FETCH (", 2); + + // Fetch changes since SEQ 6 + imap.send(&format!( + "UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)", + modseqs[6] + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("VANISHED", 1) + .assert_contains("VANISHED (EARLIER) 2") + .assert_count("FETCH (", 1); + + // Fetch changes since SEQ 7 + imap.send(&format!( + "UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)", + modseqs[7] + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("VANISHED", 1) + .assert_contains("VANISHED (EARLIER) 2") + .assert_count("FETCH (", 0); + + // Fetch changes since SEQ 8 + imap.send(&format!( + "UID FETCH 1:* (FLAGS) (CHANGEDSINCE {} VANISHED)", + modseqs[8] + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("VANISHED", 0) + .assert_count("FETCH (", 0); + + // Search since MODSEQ + imap.send(&format!("SEARCH RETURN (ALL) MODSEQ {}", modseqs[3])) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("ALL 1:3 MODSEQ"); + + imap_check + .send(&format!("SEARCH MODSEQ {}", modseqs[4])) + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("SEARCH 2 3 (MODSEQ"); + + // Store unchanged since + imap.send(&format!( + "UID STORE 2:5 (UNCHANGEDSINCE {}) +FLAGS.SILENT (\\Junk)", + modseqs[5] + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::No) + .await + .assert_contains("* 1 FETCH") + .assert_contains("UID 3)") + .assert_count("FETCH (", 1) + .assert_contains("[MODIFIED 2,4:5]"); + + imap.send(&format!( + "UID STORE 4,5 (UNCHANGEDSINCE {}) -FLAGS.SILENT (\\Answered)", + modseqs[6] + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* 2 FETCH") + .assert_contains("UID 4)") + .assert_count("FETCH (", 1) + .assert_contains("[MODIFIED 5]"); + + // QResync + imap.send("STATUS Pecorino (UIDVALIDITY)").await; + let uid_validity = imap + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_uid_validity(); + + imap.send(&format!( + "SELECT Pecorino (QRESYNC ({} {} 1:5)) ", + uid_validity, modseqs[6] + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("FETCH (", 3) + .assert_contains("VANISHED (EARLIER) 2"); +} diff --git a/tests/src/imap/copy_move.rs b/tests/src/imap/copy_move.rs new file mode 100644 index 00000000..2fd7c8b8 --- /dev/null +++ b/tests/src/imap/copy_move.rs @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::ResponseType; + +use super::{AssertResult, ImapConnection, Type}; + +pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { + // Select INBOX + imap.send("SELECT INBOX").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Copying to "All Mail" or the same mailbox should fail + imap.send("COPY 1:* INBOX").await; + imap.assert_read(Type::Tagged, ResponseType::No) + .await + .assert_response_code("CANNOT"); + + imap.send("COPY 1:* \"All Mail\"").await; + imap.assert_read(Type::Tagged, ResponseType::No) + .await + .assert_response_code("CANNOT"); + + // Copying to a non-existent mailbox should fail + imap.send("COPY 1:* \"/dev/null\"").await; + imap.assert_read(Type::Tagged, ResponseType::No) + .await + .assert_response_code("TRYCREATE"); + + // Create test folders + imap.send("CREATE \"Scamorza Affumicata\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("CREATE \"Burrata al Tartufo\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Copy messages + imap.send("COPY 1,3,5,7 \"Scamorza Affumicata\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("COPYUID") + .assert_contains("1:4"); + + // Check status + imap.send("STATUS \"Scamorza Affumicata\" (UIDNEXT MESSAGES UNSEEN SIZE)") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("MESSAGES 4") + .assert_contains("UNSEEN 4") + .assert_contains("UIDNEXT 5") + .assert_contains("SIZE 5851"); + + // Move all messages to Burrata + imap.send("SELECT \"Scamorza Affumicata\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap.send("MOVE 1:* \"Burrata al Tartufo\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* OK [COPYUID") + .assert_contains("1:4") + .assert_contains("* 1 EXPUNGE") + .assert_contains("* 1 EXPUNGE") + .assert_contains("* 1 EXPUNGE") + .assert_contains("* 1 EXPUNGE"); + + // Check status + imap.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("\"Burrata al Tartufo\" (UIDNEXT 5 MESSAGES 4 UNSEEN 4 SIZE 5851)") + .assert_contains("\"Scamorza Affumicata\" (UIDNEXT 5 MESSAGES 0 UNSEEN 0 SIZE 0)") + .assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193)"); + + // Move the messages back to Scamorza, UIDNEXT should increase. + imap.send("SELECT \"Burrata al Tartufo\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap.send("MOVE 1:* \"Scamorza Affumicata\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* OK [COPYUID") + .assert_contains("5:8") + .assert_contains("* 1 EXPUNGE") + .assert_contains("* 1 EXPUNGE") + .assert_contains("* 1 EXPUNGE") + .assert_contains("* 1 EXPUNGE"); + + // Check status + imap.send("LIST \"\" % RETURN (STATUS (UIDNEXT MESSAGES UNSEEN SIZE))") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("\"Burrata al Tartufo\" (UIDNEXT 5 MESSAGES 0 UNSEEN 0 SIZE 0)") + .assert_contains("\"Scamorza Affumicata\" (UIDNEXT 9 MESSAGES 4 UNSEEN 4 SIZE 5851)") + .assert_contains("\"INBOX\" (UIDNEXT 11 MESSAGES 10 UNSEEN 10 SIZE 12193)"); +} diff --git a/tests/src/imap/fetch.rs b/tests/src/imap/fetch.rs new file mode 100644 index 00000000..59507733 --- /dev/null +++ b/tests/src/imap/fetch.rs @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::ResponseType; + +use super::{AssertResult, ImapConnection, Type}; + +pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { + // Examine INBOX + imap.send("EXAMINE INBOX").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("10 EXISTS") + .assert_contains("[UIDNEXT 11]"); + + // Fetch all properties available from JMAP + imap.send(concat!( + "FETCH 10 (FLAGS INTERNALDATE PREVIEW EMAILID THREADID ", + "RFC822.SIZE UID ENVELOPE BODYSTRUCTURE)" + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("FLAGS (flag_009)") + .assert_contains("RFC822.SIZE 1457") + .assert_contains("UID 10") + .assert_contains("INTERNALDATE") + .assert_contains("THREADID (") + .assert_contains("EMAILID (") + .assert_contains("but then I thought, why not do both?") + .assert_contains(concat!( + "ENVELOPE (\"Sat, 20 Nov 2021 22:22:01 +0000\" ", + "\"Why not both importing AND exporting? ☺\" ", + "((\"Art Vandelay (Vandelay Industries)\" NIL \"art\" \"vandelay.com\")) ", + "((\"Art Vandelay (Vandelay Industries)\" NIL \"art\" \"vandelay.com\")) ", + "((\"Art Vandelay (Vandelay Industries)\" NIL \"art\" \"vandelay.com\")) ", + "((NIL NIL \"Colleagues\" NIL)", + "(\"James Smythe\" NIL \"james\" \"vandelay.com\")", + "(NIL NIL NIL NIL)(NIL NIL \"Friends\" NIL)", + "(NIL NIL \"jane\" \"example.com\")", + "(\"John Smîth\" NIL \"john\" \"example.com\")", + "(NIL NIL NIL NIL)) NIL NIL NIL NIL)" + )) + .assert_contains(concat!( + "BODYSTRUCTURE ((\"text\" \"html\" (\"charset\" \"us-ascii\") NIL NIL ", + "\"base64\" 239 3 \"07aab44e51c5f1833a5d19f2e1804c4b\" NIL NIL NIL) ", + "(\"message\" \"rfc822\" NIL NIL NIL NIL 723 ", + "(NIL \"Exporting my book about coffee tables\" ", + "((\"Cosmo Kramer\" NIL \"kramer\" \"kramerica.com\")) ", + "((\"Cosmo Kramer\" NIL \"kramer\" \"kramerica.com\")) ", + "((\"Cosmo Kramer\" NIL \"kramer\" \"kramerica.com\")) ", + "NIL NIL NIL NIL NIL) ", + "((\"text\" \"plain\" (\"charset\" \"utf-16\") NIL NIL ", + "\"quoted-printable\" 228 3 \"3a942a99cdd8a099ae107d3867ec20fb\" NIL NIL NIL) ", + "(\"image\" \"gif\" (\"name\" \"Book about ☕ tables.gif\") ", + "NIL NIL \"Base64\" 56 \"d40fa7f401e9dc2df56cbb740d65ff52\" ", + "(\"attachment\" ()) NIL NIL) \"mixed\" (\"boundary\" \"giddyup\") NIL NIL NIL)", + " 0 \"cdb0382a03a15601fb1b3c7422521620\" NIL NIL NIL) ", + "\"mixed\" (\"boundary\" \"festivus\") NIL NIL NIL)" + )); + + // Fetch bodyparts + imap.send(concat!( + "UID FETCH 10 (BINARY[1] BINARY.SIZE[1] BODY[1.TEXT] BODY[2.1.HEADER] ", + "BINARY[2.1] BODY[MIME] BODY[HEADER.FIELDS (From)]<10.8>)" + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("BINARY[1] {175}") + .assert_contains("BINARY.SIZE[1] 175") + .assert_contains("BODY[1.TEXT] {239}") + .assert_contains("BODY[2.1.HEADER] {88}") + .assert_contains("BINARY[2.1] {101}") + .assert_contains("BODY[MIME] {54}") + .assert_contains("BODY[HEADER.FIELDS (FROM)]<10> {8}") + .assert_contains("“exporting”") + .assert_contains("PGh0bWw+PHA+") + .assert_contains("Content-Transfer-Encoding: quoted-printable") + .assert_contains("ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨") + .assert_contains("Vandelay"); + + // We are in EXAMINE mode, fetching body should not set \Seen + imap.send("UID FETCH 10 (FLAGS)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("FLAGS (flag_009)"); + + // Switch to SELECT mode + imap.send("SELECT INBOX").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Peek bodyparts + imap.send("UID FETCH 10 (BINARY.PEEK[1] BINARY.SIZE[1] BODY.PEEK[1.TEXT])") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("BINARY[1] {175}") + .assert_contains("BINARY.SIZE[1] 175") + .assert_contains("BODY[1.TEXT] {239}"); + + // PEEK was used, \Seen should not be set + imap.send("UID FETCH 10 (FLAGS)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("FLAGS (flag_009)"); + + // Fetching a body section should set the \Seen flag + imap.send("UID FETCH 10 (BODY[1.TEXT])").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("FLAGS") + .assert_contains("\\Seen"); + + // Fetch a sequence + imap.send("FETCH 1:5,7:10 (UID FLAGS)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* 1 FETCH (UID 1 ") + .assert_contains("* 2 FETCH (UID 2 ") + .assert_contains("* 3 FETCH (UID 3 ") + .assert_contains("* 4 FETCH (UID 4 ") + .assert_contains("* 5 FETCH (UID 5 ") + .assert_contains("* 7 FETCH (UID 7 ") + .assert_contains("* 8 FETCH (UID 8 ") + .assert_contains("* 9 FETCH (UID 9 ") + .assert_contains("* 10 FETCH (UID 10 "); + + imap.send("FETCH 7:* (UID FLAGS)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* 7 FETCH (UID 7 ") + .assert_contains("* 8 FETCH (UID 8 ") + .assert_contains("* 9 FETCH (UID 9 ") + .assert_contains("* 10 FETCH (UID 10 "); + + // Fetch using a saved search + imap.send("UID SEARCH RETURN (SAVE) FROM \"nathaniel\"") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("FETCH $ (UID PREVIEW)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* 1 FETCH (UID 1 ") + .assert_contains("* 4 FETCH (UID 4 ") + .assert_contains("* 6 FETCH (UID 6 ") + .assert_contains("Some text appears here") + .assert_contains("plain text version of message goes here") + .assert_contains("This is implicitly typed plain US-ASCII text."); +} diff --git a/tests/src/imap/idle.rs b/tests/src/imap/idle.rs new file mode 100644 index 00000000..16995611 --- /dev/null +++ b/tests/src/imap/idle.rs @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::ResponseType; + +use super::{AssertResult, ImapConnection, Type}; + +pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) { + // Switch connection to IDLE mode + imap_check.send("CREATE Parmeggiano").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check.send("SELECT Parmeggiano").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check.send("NOOP").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check.send("IDLE").await; + imap_check + .assert_read(Type::Continuation, ResponseType::Ok) + .await; + + // Expect a new mailbox update + imap.send("CREATE Provolone").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("LIST () \"/\" \"Provolone\""); + + // Insert a message in the new folder and expect an update + let message = "From: test@domain.com\nSubject: Test\n\nTest message\n"; + imap.send(&format!("APPEND Provolone {{{}}}", message.len())) + .await; + imap.assert_read(Type::Continuation, ResponseType::Ok).await; + imap.send_untagged(message).await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("STATUS \"Provolone\"") + .assert_contains("MESSAGES 1") + .assert_contains("UNSEEN 1") + .assert_contains("UIDNEXT 2"); + + // Change message to Seen and expect an update + imap.send("SELECT Provolone").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("STORE 1:* +FLAGS (\\Seen)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("STATUS \"Provolone\"") + .assert_contains("MESSAGES 1") + .assert_contains("UNSEEN 0") + .assert_contains("UIDNEXT 2"); + + // Delete message and expect an update + imap.send("STORE 1:* +FLAGS (\\Deleted)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("CLOSE").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("STATUS \"Provolone\"") + .assert_contains("MESSAGES 0") + .assert_contains("UNSEEN 0") + .assert_contains("UIDNEXT 2"); + + // Delete folder and expect an update + imap.send("DELETE Provolone").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("LIST (\\NonExistent) \"/\" \"Provolone\""); + + // Add a message to Inbox and expect an update + imap.send(&format!("APPEND Parmeggiano {{{}}}", message.len())) + .await; + imap.assert_read(Type::Continuation, ResponseType::Ok).await; + imap.send_untagged(message).await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("MESSAGES 1") + .assert_contains("UNSEEN 1"); + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("* 1 EXISTS"); + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("* 1 FETCH (FLAGS () UID 1)"); + + // Delete message and expect an update + imap.send("SELECT Parmeggiano").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap.send("STORE 1 +FLAGS (\\Deleted)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("* 1 FETCH (FLAGS (\\Deleted) UID 1)"); + + imap.send("UID EXPUNGE").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("* 1 EXPUNGE") + .assert_contains("* 0 EXISTS"); + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("MESSAGES 0") + .assert_contains("UNSEEN 0"); + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("* 1 EXPUNGE"); + imap_check + .assert_read(Type::Status, ResponseType::Ok) + .await + .assert_contains("* 0 EXISTS"); + + // Stop IDLE mode + imap_check.send_raw("DONE").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap_check.send("NOOP").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; +} diff --git a/tests/src/imap/mailbox.rs b/tests/src/imap/mailbox.rs new file mode 100644 index 00000000..d687b1a7 --- /dev/null +++ b/tests/src/imap/mailbox.rs @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::ResponseType; + +use super::{AssertResult, ImapConnection, Type}; + +pub async fn test(mut imap: &mut ImapConnection, mut imap_check: &mut ImapConnection) { + // List folders + imap.send("LIST \"\" \"*\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [ + ("All Mail", ["NoInferiors"]), + ("INBOX", [""]), + ("Deleted Items", [""]), + ], + true, + ); + + // Create folders + imap.send("CREATE \"Tofu\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("CREATE \"Fruit\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("CREATE \"Fruit/Apple\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("CREATE \"Fruit/Apple/Green\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + for imap in [&mut imap, &mut imap_check] { + imap.send("LIST \"\" \"*\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [ + ("All Mail", ["NoInferiors"]), + ("INBOX", [""]), + ("Deleted Items", [""]), + ("Fruit", [""]), + ("Fruit/Apple", [""]), + ("Fruit/Apple/Green", [""]), + ("Tofu", [""]), + ], + true, + ); + } + + // Folders under All Mail should not be allowed + imap.send("CREATE \"All Mail/Untitled\"").await; + imap.assert_read(Type::Tagged, ResponseType::No).await; + + // Enable IMAP4rev2 + imap.send("ENABLE IMAP4rev2").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Create missing parent folders + imap.send("CREATE \"/Vegetable/Broccoli\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("[MAILBOXID ("); + + imap.send("CREATE \" Cars/Electric /4 doors/ Red/\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + for imap in [&mut imap, &mut imap_check] { + imap.send("LIST \"\" \"*\" RETURN (CHILDREN SPECIAL-USE)") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [ + ("All Mail", ["NoInferiors", "All"]), + ("INBOX", ["HasNoChildren", ""]), + ("Deleted Items", ["HasNoChildren", "Trash"]), + ("Cars/Electric/4 doors/Red", ["HasNoChildren", ""]), + ("Cars/Electric/4 doors", ["HasChildren", ""]), + ("Cars/Electric", ["HasChildren", ""]), + ("Cars", ["HasChildren", ""]), + ("Fruit", ["HasChildren", ""]), + ("Fruit/Apple", ["HasChildren", ""]), + ("Fruit/Apple/Green", ["HasNoChildren", ""]), + ("Vegetable", ["HasChildren", ""]), + ("Vegetable/Broccoli", ["HasNoChildren", ""]), + ("Tofu", ["HasNoChildren", ""]), + ], + true, + ); + } + + // Rename folders + imap.send("RENAME \"Fruit/Apple/Green\" \"Fruit/Apple/Red\"") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("RENAME \"Cars\" \"Vehicles\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("RENAME \"Vegetable/Broccoli\" \"Veggies/Green/Broccoli\"") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("RENAME \"Tofu\" \"INBOX\"").await; + imap.assert_read(Type::Tagged, ResponseType::No).await; + imap.send("RENAME \"Tofu\" \"INBOX/Tofu\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("RENAME \"Deleted Items\" \"Recycle Bin\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + for imap in [&mut imap, &mut imap_check] { + imap.send("LIST \"\" \"*\" RETURN (CHILDREN SPECIAL-USE)") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [ + ("All Mail", ["NoInferiors", "All"]), + ("INBOX", ["HasChildren", ""]), + ("INBOX/Tofu", ["HasNoChildren", ""]), + ("Recycle Bin", ["HasNoChildren", "Trash"]), + ("Vehicles/Electric/4 doors/Red", ["HasNoChildren", ""]), + ("Vehicles/Electric/4 doors", ["HasChildren", ""]), + ("Vehicles/Electric", ["HasChildren", ""]), + ("Vehicles", ["HasChildren", ""]), + ("Fruit", ["HasChildren", ""]), + ("Fruit/Apple", ["HasChildren", ""]), + ("Fruit/Apple/Red", ["HasNoChildren", ""]), + ("Vegetable", ["HasNoChildren", ""]), + ("Veggies", ["HasChildren", ""]), + ("Veggies/Green", ["HasChildren", ""]), + ("Veggies/Green/Broccoli", ["HasNoChildren", ""]), + ], + true, + ); + } + + // Delete folders + imap.send("DELETE \"INBOX/Tofu\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("DELETE \"Vegetable\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("DELETE \"All Mail\"").await; + imap.assert_read(Type::Tagged, ResponseType::No).await; + imap.send("DELETE \"Vehicles\"").await; + imap.assert_read(Type::Tagged, ResponseType::No).await; + for imap in [&mut imap, &mut imap_check] { + imap.send("LIST \"\" \"*\" RETURN (CHILDREN SPECIAL-USE)") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [ + ("All Mail", ["NoInferiors", "All"]), + ("INBOX", ["HasNoChildren", ""]), + ("Recycle Bin", ["HasNoChildren", "Trash"]), + ("Vehicles/Electric/4 doors/Red", ["HasNoChildren", ""]), + ("Vehicles/Electric/4 doors", ["HasChildren", ""]), + ("Vehicles/Electric", ["HasChildren", ""]), + ("Vehicles", ["HasChildren", ""]), + ("Fruit", ["HasChildren", ""]), + ("Fruit/Apple", ["HasChildren", ""]), + ("Fruit/Apple/Red", ["HasNoChildren", ""]), + ("Veggies", ["HasChildren", ""]), + ("Veggies/Green", ["HasChildren", ""]), + ("Veggies/Green/Broccoli", ["HasNoChildren", ""]), + ], + true, + ); + } + + // Subscribe + imap.send("SUBSCRIBE \"INBOX\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("SUBSCRIBE \"Vehicles/Electric/4 doors/Red\"") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + for imap in [&mut imap, &mut imap_check] { + imap.send("LIST \"\" \"*\" RETURN (SUBSCRIBED SPECIAL-USE)") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [ + ("All Mail", ["NoInferiors", "All"]), + ("INBOX", ["Subscribed", ""]), + ("Recycle Bin", ["", "Trash"]), + ("Vehicles/Electric/4 doors/Red", ["Subscribed", ""]), + ("Vehicles/Electric/4 doors", ["", ""]), + ("Vehicles/Electric", ["", ""]), + ("Vehicles", ["", ""]), + ("Fruit", ["", ""]), + ("Fruit/Apple", ["", ""]), + ("Fruit/Apple/Red", ["", ""]), + ("Veggies", ["", ""]), + ("Veggies/Green", ["", ""]), + ("Veggies/Green/Broccoli", ["", ""]), + ], + true, + ); + } + + // Filter by subscribed including children + imap.send("LIST (SUBSCRIBED) \"\" \"*\" RETURN (CHILDREN)") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [ + ("INBOX", ["Subscribed", "HasNoChildren"]), + ( + "Vehicles/Electric/4 doors/Red", + ["Subscribed", "HasNoChildren"], + ), + ], + true, + ); + + // Recursive match including children + imap.send("LIST (SUBSCRIBED RECURSIVEMATCH) \"\" \"*\" RETURN (CHILDREN)") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [ + ("INBOX", ["Subscribed", "HasNoChildren"]), + ( + "Vehicles/Electric/4 doors/Red", + ["Subscribed", "HasNoChildren"], + ), + ( + "Vehicles/Electric/4 doors", + ["\"CHILDINFO\" (\"SUBSCRIBED\")", "HasChildren"], + ), + ( + "Vehicles/Electric", + ["\"CHILDINFO\" (\"SUBSCRIBED\")", "HasChildren"], + ), + ( + "Vehicles", + ["\"CHILDINFO\" (\"SUBSCRIBED\")", "HasChildren"], + ), + ], + true, + ); + + // Imap4rev1 LSUB + imap.send("LSUB \"\" \"*\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [("INBOX", [""]), ("Vehicles/Electric/4 doors/Red", [""])], + true, + ); + + // Unsubscribe + imap.send("UNSUBSCRIBE \"Vehicles/Electric/4 doors/Red\"") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + for imap in [&mut imap, &mut imap_check] { + imap.send("LIST (SUBSCRIBED RECURSIVEMATCH) \"\" \"*\" RETURN (CHILDREN)") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders([("INBOX", ["Subscribed", "HasNoChildren"])], true); + } + + // LIST Filters + imap.send("LIST \"\" \"%\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [ + ("All Mail", [""]), + ("INBOX", [""]), + ("Recycle Bin", [""]), + ("Vehicles", [""]), + ("Fruit", [""]), + ("Veggies", [""]), + ], + true, + ); + + imap.send("LIST \"\" \"*/Red\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders( + [ + ("Vehicles/Electric/4 doors/Red", [""]), + ("Fruit/Apple/Red", [""]), + ], + true, + ); + + imap.send("LIST \"\" \"Fruit/*\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders([("Fruit/Apple/Red", [""]), ("Fruit/Apple", [""])], true); + + imap.send("LIST \"\" \"Fruit/%\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_folders([("Fruit/Apple", [""])], true); + + // Restore Trash folder's original name + imap.send("RENAME \"Recycle Bin\" \"Deleted Items\"").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; +} diff --git a/tests/src/imap/managesieve.rs b/tests/src/imap/managesieve.rs new file mode 100644 index 00000000..40cbf8bc --- /dev/null +++ b/tests/src/imap/managesieve.rs @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::time::Duration; + +use imap_proto::ResponseType; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf}, + net::TcpStream, +}; + +use super::AssertResult; + +pub async fn test() { + // Connect to ManageSieve + let mut sieve = SieveConnection::connect().await; + sieve + .assert_read(ResponseType::Ok) + .await + .assert_contains("IMPLEMENTATION"); + + // Authenticate + sieve + .send("AUTHENTICATE \"PLAIN\" \"AGpkb2VAZXhhbXBsZS5jb20Ac2VjcmV0\"") + .await; + sieve + .assert_read(ResponseType::Ok) + .await + .assert_contains("MAXREDIRECTS"); + + // CheckScript + sieve.send("CHECKSCRIPT \"if true { keep; }\"").await; + sieve.assert_read(ResponseType::Ok).await; + sieve.send("CHECKSCRIPT \"keep :invalidtag;\"").await; + sieve.assert_read(ResponseType::No).await; + + // PutScript + sieve + .send_literal("PUTSCRIPT \"simple script\" ", "if true { keep; }\r\n") + .await; + sieve.assert_read(ResponseType::Ok).await; + sieve + .send_literal( + "PUTSCRIPT \"holidays\" ", + "require \"vacation\"; vacation \"Gone fishin'\";\r\n", + ) + .await; + sieve.assert_read(ResponseType::Ok).await; + sieve.send("PUTSCRIPT \"holidays\" \"discard;\"").await; + sieve + .assert_read(ResponseType::No) + .await + .assert_contains("ALREADYEXISTS"); + + // GetScript + sieve.send("GETSCRIPT \"simple script\"").await; + sieve + .assert_read(ResponseType::Ok) + .await + .assert_contains("if true"); + sieve.send("GETSCRIPT \"holidays\"").await; + sieve + .assert_read(ResponseType::Ok) + .await + .assert_contains("Gone fishin'"); + sieve.send("GETSCRIPT \"dummy\"").await; + sieve.assert_read(ResponseType::No).await; + + // ListScripts + sieve.send("LISTSCRIPTS").await; + sieve + .assert_read(ResponseType::Ok) + .await + .assert_contains("simple script") + .assert_contains("holidays") + .assert_count("ACTIVE", 0); + + // RenameScript + sieve + .send("RENAMESCRIPT \"simple script\" \"minimalist script\"") + .await; + sieve.assert_read(ResponseType::Ok).await; + sieve + .send("RENAMESCRIPT \"holidays\" \"minimalist script\"") + .await; + sieve + .assert_read(ResponseType::No) + .await + .assert_contains("ALREADYEXISTS"); + + // SetActive + sieve.send("SETACTIVE \"holidays\"").await; + sieve.assert_read(ResponseType::Ok).await; + + sieve.send("LISTSCRIPTS").await; + sieve + .assert_read(ResponseType::Ok) + .await + .assert_contains("minimalist script") + .assert_contains("holidays\" ACTIVE"); + + // Deleting an active script should not be allowed + sieve.send("DELETESCRIPT \"holidays\"").await; + sieve + .assert_read(ResponseType::No) + .await + .assert_contains("ACTIVE"); + + // Deactivate all + sieve.send("SETACTIVE \"\"").await; + sieve.assert_read(ResponseType::Ok).await; + + sieve.send("LISTSCRIPTS").await; + sieve + .assert_read(ResponseType::Ok) + .await + .assert_contains("minimalist script") + .assert_contains("holidays") + .assert_count("ACTIVE", 0); + + // DeleteScript + sieve.send("DELETESCRIPT \"holidays\"").await; + sieve.assert_read(ResponseType::Ok).await; + sieve.send("DELETESCRIPT \"minimalist script\"").await; + sieve.assert_read(ResponseType::Ok).await; + + sieve.send("LISTSCRIPTS").await; + sieve + .assert_read(ResponseType::Ok) + .await + .assert_count("minimalist script", 0) + .assert_count("holidays", 0); +} + +pub struct SieveConnection { + reader: Lines>>, + writer: WriteHalf, +} + +impl SieveConnection { + pub async fn connect() -> Self { + let (reader, writer) = + tokio::io::split(TcpStream::connect("127.0.0.1:4190").await.unwrap()); + SieveConnection { + reader: BufReader::new(reader).lines(), + writer, + } + } + + pub async fn assert_read(&mut self, rt: ResponseType) -> Vec { + let lines = self.read().await; + let mut buf = Vec::with_capacity(10); + rt.serialize(&mut buf); + if lines + .last() + .unwrap() + .starts_with(&String::from_utf8(buf).unwrap()) + { + lines + } else { + panic!("Expected {:?} from server but got: {:?}", rt, lines); + } + } + + pub async fn read(&mut self) -> Vec { + let mut lines = Vec::new(); + loop { + match tokio::time::timeout(Duration::from_millis(1500), self.reader.next_line()).await { + Ok(Ok(Some(line))) => { + let is_done = + line.starts_with("OK") || line.starts_with("NO") || line.starts_with("BYE"); + println!("<- {:?}", line); + lines.push(line); + if is_done { + return lines; + } + } + Ok(Ok(None)) => { + panic!("Invalid response: {:?}.", lines); + } + Ok(Err(err)) => { + panic!("Connection broken: {} ({:?})", err, lines); + } + Err(_) => panic!("Timeout while waiting for server response: {:?}", lines), + } + } + } + + pub async fn send(&mut self, text: &str) { + println!("-> {:?}", text); + self.writer.write_all(text.as_bytes()).await.unwrap(); + self.writer.write_all(b"\r\n").await.unwrap(); + } + + pub async fn send_raw(&mut self, text: &str) { + println!("-> {:?}", text); + self.writer.write_all(text.as_bytes()).await.unwrap(); + } + + pub async fn send_literal(&mut self, text: &str, literal: &str) { + self.send(&format!("{}{{{}+}}\r\n{}", text, literal.len(), literal)) + .await; + } +} diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index 665a37dc..027243fa 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -1 +1,630 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +pub mod acl; +pub mod append; +pub mod basic; pub mod body_structure; +pub mod condstore; +pub mod copy_move; +pub mod fetch; +pub mod idle; +pub mod mailbox; +pub mod managesieve; +pub mod search; +pub mod store; +pub mod thread; + +use std::{path::PathBuf, sync::Arc, time::Duration}; + +use ::managesieve::core::ManageSieveSessionManager; +use directory::config::ConfigDirectory; +use imap::core::{ImapSessionManager, IMAP}; +use imap_proto::ResponseType; +use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP}; +use smtp::core::SMTP; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Lines, ReadHalf, WriteHalf}, + net::TcpStream, + sync::{mpsc, watch}, +}; +use utils::{config::ServerProtocol, UnwrapFailure}; + +use crate::{ + add_test_certs, + directory::sql::{ + add_to_group_id, create_test_directory, create_test_user, create_test_user_with_email, + }, + store::TempDir, +}; + +const SERVER: &str = r#" +[server] +hostname = "imap.example.org" + +[server.listener.imap] +bind = ["127.0.0.1:9991"] +protocol = "imap" +max-connections = 81920 + +[server.listener.imaptls] +bind = ["127.0.0.1:9992"] +protocol = "imap" +max-connections = 81920 +tls.implict = true + +[server.listener.sieve] +bind = ["127.0.0.1:4190"] +protocol = "managesieve" +max-connections = 81920 + +[server.socket] +reuse-addr = true + +[server.tls] +enable = true +implicit = false +certificate = "default" + +[session.ehlo] +reject-non-fqdn = false + +[session.rcpt] +relay = [ { if = "authenticated-as", ne = "", then = true }, + { else = false } ] +directory = "sql" + +[session.rcpt.errors] +total = 5 +wait = "1ms" + +[queue] +path = "{TMP}" +hash = 64 + +[report] +path = "{TMP}" +hash = 64 + +[resolver] +type = "system" + +[queue.outbound] +next-hop = [ { if = "rcpt-domain", in-list = "local/domains", then = "local" }, + { if = "rcpt-domain", in-list = "local/remote-domains", then = "mock-smtp" }, + { else = false } ] + +[remote."mock-smtp"] +address = "localhost" +port = 9999 +protocol = "smtp" + +[remote."mock-smtp".tls] +implicit = false +allow-invalid-certs = true + +[session.extensions] +future-release = [ { if = "authenticated-as", ne = "", then = "99999999d"}, + { else = false } ] + +[store] +db.path = "{TMP}/sqlite.db" + +[store.blob] +type = "local" + +[store.blob.local] +path = "{TMP}" + +[certificate.default] +cert = "file://{CERT}" +private-key = "file://{PK}" + +[jmap] +directory = "sql" + +[jmap.protocol] +set.max-objects = 100000 + +[jmap.protocol.request] +max-concurrent = 8 + +[jmap.protocol.upload] +max-size = 5000000 +max-concurrent = 4 +ttl = "1m" + +[jmap.protocol.upload.quota] +files = 3 +size = 50000 + +[jmap.rate-limit] +account.rate = "1000/1m" +authentication.rate = "100/2s" +anonymous.rate = "100/1m" + +[jmap.event-source] +throttle = "500ms" + +[jmap.web-sockets] +throttle = "500ms" + +[jmap.push] +throttle = "500ms" +attempts.interval = "500ms" + +[directory."sql"] +type = "sql" +address = "sqlite::memory:" + +[directory."sql".pool] +max-connections = 1 + +[directory."sql".query] +login = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true AND type = 'individual'" +name = "SELECT id, name, type, secret, description, quota FROM accounts WHERE name = ?" +id = "SELECT id, name, type, secret, description, quota FROM accounts WHERE id = ?" +members = "SELECT gid FROM group_members WHERE uid = ?" +recipients = "SELECT id FROM emails WHERE address = ?" +emails = "SELECT address FROM emails WHERE id = ? AND type != 'list' ORDER BY type DESC, address ASC" +verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5" +expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.id = l.id WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" +domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || ? LIMIT 1" + +[directory."sql".columns] +name = "name" +description = "description" +secret = "secret" +id = "id" +email = "address" +quota = "quota" +type = "type" + +[directory."local"] +type = "memory" + +[directory."local".lookup] +domains = ["example.com"] +remote-domains = ["remote.org", "foobar.com", "test.com", "other_domain.com"] + +[oauth] +key = "parerga_und_paralipomena" +max-auth-attempts = 1 + +[oauth.expiry] +user-code = "1s" +token = "1s" +refresh-token = "3s" +refresh-token-renew = "2s" +"#; + +#[allow(dead_code)] +struct IMAPTest { + jmap: Arc, + imap: Arc, + temp_dir: TempDir, + shutdown_tx: watch::Sender, +} + +async fn init_imap_tests(delete_if_exists: bool) -> IMAPTest { + // Load and parse config + let temp_dir = TempDir::new("imap_tests", delete_if_exists); + let config = utils::config::Config::parse( + &add_test_certs(SERVER).replace("{TMP}", &temp_dir.path.display().to_string()), + ) + .unwrap(); + let servers = config.parse_servers().unwrap(); + let directory = config.parse_directory().unwrap(); + + // Start JMAP and SMTP servers + servers.bind(&config); + let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER); + let smtp = SMTP::init(&config, &servers, &directory, delivery_tx) + .await + .failed("Invalid configuration file"); + let jmap = JMAP::init(&config, &directory, delivery_rx, smtp.clone()) + .await + .failed("Invalid configuration file"); + let imap: Arc = IMAP::init(&config) + .await + .failed("Invalid configuration file"); + let shutdown_tx = servers.spawn(|server, shutdown_rx| { + match &server.protocol { + ServerProtocol::Jmap => { + server.spawn(JmapSessionManager::new(jmap.clone()), shutdown_rx) + } + ServerProtocol::Imap => server.spawn( + ImapSessionManager::new(jmap.clone(), imap.clone()), + shutdown_rx, + ), + ServerProtocol::ManageSieve => server.spawn( + ManageSieveSessionManager::new(jmap.clone(), imap.clone()), + shutdown_rx, + ), + _ => unreachable!(), + }; + }); + + // Create tables and test accounts + create_test_directory(jmap.directory.as_ref()).await; + create_test_user(jmap.directory.as_ref(), "admin", "secret", "Superuser").await; + add_to_group_id(jmap.directory.as_ref(), "admin", 0).await; + create_test_user_with_email( + jmap.directory.as_ref(), + "jdoe@example.com", + "secret", + "John Doe", + ) + .await; + create_test_user_with_email( + jmap.directory.as_ref(), + "jane.smith@example.com", + "secret", + "Jane Smith", + ) + .await; + create_test_user_with_email( + jmap.directory.as_ref(), + "foobar@example.com", + "secret", + "Bill Foobar", + ) + .await; + + if delete_if_exists { + jmap.store.destroy().await; + } + + IMAPTest { + jmap, + imap, + temp_dir, + shutdown_tx, + } +} + +#[tokio::test] +pub async fn imap_tests() { + // Prepare settings + let delete = true; + let handle = init_imap_tests(delete).await; + + // Connect to IMAP server + let mut imap_check = ImapConnection::connect(b"_y ").await; + let mut imap = ImapConnection::connect(b"_x ").await; + for imap in [&mut imap, &mut imap_check] { + imap.assert_read(Type::Untagged, ResponseType::Ok).await; + } + + // Unauthenticated tests + basic::test(&mut imap, &mut imap_check).await; + + // Login + for imap in [&mut imap, &mut imap_check] { + imap.send("AUTHENTICATE PLAIN {32+}\r\nAGpkb2VAZXhhbXBsZS5jb20Ac2VjcmV0") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + } + + // Delete folders + for mailbox in ["Drafts", "Junk Mail", "Sent Items"] { + imap.send(&format!("DELETE \"{}\"", mailbox)).await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + } + + mailbox::test(&mut imap, &mut imap_check).await; + append::test(&mut imap, &mut imap_check).await; + search::test(&mut imap, &mut imap_check).await; + fetch::test(&mut imap, &mut imap_check).await; + store::test(&mut imap, &mut imap_check).await; + copy_move::test(&mut imap, &mut imap_check).await; + thread::test(&mut imap, &mut imap_check).await; + idle::test(&mut imap, &mut imap_check).await; + condstore::test(&mut imap, &mut imap_check).await; + acl::test(&mut imap, &mut imap_check).await; + + // Logout + for imap in [&mut imap, &mut imap_check] { + imap.send("UNAUTHENTICATE").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + imap.send("LOGOUT").await; + imap.assert_read(Type::Untagged, ResponseType::Bye).await; + } + + // Run ManageSieve tests + managesieve::test().await; + + // Remove test data + if delete { + handle.temp_dir.delete(); + } +} + +pub struct ImapConnection { + tag: &'static [u8], + reader: Lines>>, + writer: WriteHalf, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Type { + Tagged, + Untagged, + Continuation, + Status, +} + +impl ImapConnection { + pub async fn connect(tag: &'static [u8]) -> Self { + let (reader, writer) = + tokio::io::split(TcpStream::connect("127.0.0.1:9991").await.unwrap()); + ImapConnection { + tag, + reader: BufReader::new(reader).lines(), + writer, + } + } + + pub async fn assert_read(&mut self, t: Type, rt: ResponseType) -> Vec { + let lines = self.read(t).await; + let mut buf = Vec::with_capacity(10); + buf.extend_from_slice(match t { + Type::Tagged => self.tag, + Type::Untagged | Type::Status => b"* ", + Type::Continuation => b"+ ", + }); + if !matches!(t, Type::Continuation | Type::Status) { + rt.serialize(&mut buf); + } + if lines + .last() + .unwrap() + .starts_with(&String::from_utf8(buf).unwrap()) + { + lines + } else { + panic!("Expected {:?}/{:?} from server but got: {:?}", t, rt, lines); + } + } + + pub async fn read(&mut self, t: Type) -> Vec { + let mut lines = Vec::new(); + loop { + match tokio::time::timeout(Duration::from_millis(1500), self.reader.next_line()).await { + Ok(Ok(Some(line))) => { + let is_done = line.starts_with(match t { + Type::Tagged => std::str::from_utf8(self.tag).unwrap(), + Type::Untagged | Type::Status => "* ", + Type::Continuation => "+ ", + }); + println!("<- {:?}", line); + lines.push(line); + if is_done { + return lines; + } + } + Ok(Ok(None)) => { + panic!("Invalid response: {:?}.", lines); + } + Ok(Err(err)) => { + panic!("Connection broken: {} ({:?})", err, lines); + } + Err(_) => panic!("Timeout while waiting for server response: {:?}", lines), + } + } + } + + pub async fn send(&mut self, text: &str) { + println!("-> {}{:?}", std::str::from_utf8(self.tag).unwrap(), text); + self.writer.write_all(self.tag).await.unwrap(); + self.writer.write_all(text.as_bytes()).await.unwrap(); + self.writer.write_all(b"\r\n").await.unwrap(); + } + + pub async fn send_untagged(&mut self, text: &str) { + println!("-> {:?}", text); + self.writer.write_all(text.as_bytes()).await.unwrap(); + self.writer.write_all(b"\r\n").await.unwrap(); + } + + pub async fn send_raw(&mut self, text: &str) { + println!("-> {:?}", text); + self.writer.write_all(text.as_bytes()).await.unwrap(); + } +} + +pub trait AssertResult: Sized { + fn assert_folders<'x>( + self, + expected: impl IntoIterator)>, + match_all: bool, + ) -> Self; + + fn assert_response_code(self, code: &str) -> Self; + fn assert_contains(self, text: &str) -> Self; + fn assert_count(self, text: &str, occurences: usize) -> Self; + fn assert_equals(self, text: &str) -> Self; + fn into_response_code(self) -> String; + fn into_highest_modseq(self) -> String; + fn into_uid_validity(self) -> String; + fn into_append_uid(self) -> String; + fn into_copy_uid(self) -> String; + fn into_modseq(self) -> String; +} + +impl AssertResult for Vec { + fn assert_folders<'x>( + self, + expected: impl IntoIterator)>, + match_all: bool, + ) -> Self { + let mut match_count = 0; + 'outer: for (mailbox_name, flags) in expected.into_iter() { + for result in self.iter() { + if result.contains(&format!("\"{}\"", mailbox_name)) { + for flag in flags { + if !flag.is_empty() && !result.contains(flag) { + panic!("Expected mailbox {} to have flag {}", mailbox_name, flag); + } + } + match_count += 1; + continue 'outer; + } + } + panic!("Mailbox {} is not present.", mailbox_name); + } + if match_all && match_count != self.len() - 1 { + panic!( + "Expected {} mailboxes, but got {}", + match_count, + self.len() - 1 + ); + } + self + } + + fn assert_response_code(self, code: &str) -> Self { + if !self.last().unwrap().contains(&format!("[{}]", code)) { + panic!( + "Response code {:?} not found, got {:?}", + code, + self.last().unwrap() + ); + } + self + } + + fn assert_contains(self, text: &str) -> Self { + for line in &self { + if line.contains(text) { + return self; + } + } + panic!("Expected response to contain {:?}, got {:?}", text, self); + } + + fn assert_count(self, text: &str, occurences: usize) -> Self { + assert_eq!( + self.iter().filter(|l| l.contains(text)).count(), + occurences, + "Expected {} occurrences of {:?}, found {}.", + occurences, + text, + self.iter().filter(|l| l.contains(text)).count() + ); + self + } + + fn assert_equals(self, text: &str) -> Self { + for line in &self { + if line == text { + return self; + } + } + panic!("Expected response to be {:?}, got {:?}", text, self); + } + + fn into_response_code(self) -> String { + if let Some((_, code)) = self.last().unwrap().split_once('[') { + if let Some((code, _)) = code.split_once(']') { + return code.to_string(); + } + } + panic!("No response code found in {:?}", self.last().unwrap()); + } + + fn into_append_uid(self) -> String { + if let Some((_, code)) = self.last().unwrap().split_once("[APPENDUID ") { + if let Some((code, _)) = code.split_once(']') { + if let Some((_, uid)) = code.split_once(' ') { + return uid.to_string(); + } + } + } + panic!("No APPENDUID found in {:?}", self.last().unwrap()); + } + + fn into_copy_uid(self) -> String { + for line in &self { + if let Some((_, code)) = line.split_once("[COPYUID ") { + if let Some((code, _)) = code.split_once(']') { + if let Some((_, uid)) = code.split_once(' ') { + return uid.to_string(); + } + } + } + } + panic!("No COPYUID found in {:?}", self); + } + + fn into_highest_modseq(self) -> String { + for line in &self { + if let Some((_, value)) = line.split_once("HIGHESTMODSEQ ") { + if let Some((value, _)) = value.split_once(']') { + return value.to_string(); + } else if let Some((value, _)) = value.split_once(')') { + return value.to_string(); + } else { + panic!("No HIGHESTMODSEQ delimiter found in {:?}", line); + } + } + } + panic!("No HIGHESTMODSEQ entries found in {:?}", self); + } + + fn into_modseq(self) -> String { + for line in &self { + if let Some((_, value)) = line.split_once("MODSEQ (") { + if let Some((value, _)) = value.split_once(')') { + return value.to_string(); + } else { + panic!("No MODSEQ delimiter found in {:?}", line); + } + } + } + panic!("No MODSEQ entries found in {:?}", self); + } + + fn into_uid_validity(self) -> String { + for line in &self { + if let Some((_, value)) = line.split_once("UIDVALIDITY ") { + if let Some((value, _)) = value.split_once(']') { + return value.to_string(); + } else if let Some((value, _)) = value.split_once(')') { + return value.to_string(); + } else { + panic!("No UIDVALIDITY delimiter found in {:?}", line); + } + } + } + panic!("No UIDVALIDITY entries found in {:?}", self); + } +} + +fn resources_dir() -> PathBuf { + let mut resources = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + resources.push("resources"); + resources.push("imap"); + resources +} diff --git a/tests/src/imap/search.rs b/tests/src/imap/search.rs new file mode 100644 index 00000000..efbef21f --- /dev/null +++ b/tests/src/imap/search.rs @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::ResponseType; + +use super::{AssertResult, ImapConnection, Type}; + +pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection) { + // Searches without selecting a mailbox should fail. + imap.send("SEARCH RETURN (MIN MAX COUNT ALL) ALL").await; + imap.assert_read(Type::Tagged, ResponseType::Bad).await; + + // Select INBOX + imap.send("SELECT INBOX").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("10 EXISTS") + .assert_contains("[UIDNEXT 11]"); + imap_check.send("SELECT INBOX").await; + imap_check.assert_read(Type::Tagged, ResponseType::Ok).await; + + // Min, Max and Count + imap.send("SEARCH RETURN (MIN MAX COUNT ALL) ALL").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("COUNT 10 MIN 1 MAX 10 ALL 1,10"); + imap_check.send("UID SEARCH ALL").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 1 2 3 4 5 6 7 8 9 10"); + + // Filters + imap_check + .send("UID SEARCH OR FROM nathaniel SUBJECT argentina") + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 1 3 4 6"); + + imap_check + .send("UID SEARCH UNSEEN OR KEYWORD Flag_007 KEYWORD Flag_004") + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 5 8"); + + imap_check + .send("UID SEARCH TEXT coffee FROM vandelay SUBJECT exporting SENTON 20-Nov-2021") + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 10"); + + imap_check + .send("UID SEARCH NOT (FROM nathaniel ANSWERED)") + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 2 3 5 7 8 9 10"); + + imap_check + .send("UID SEARCH UID 0:6 LARGER 1000 SMALLER 2000") + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 1 2"); + + // Saved search + imap_check.send( + "UID SEARCH RETURN (SAVE ALL) OR OR FROM nathaniel FROM vandelay OR SUBJECT rfc FROM gore", + ) + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("1,3:4,6,8,10"); + + imap_check.send("UID SEARCH NOT $").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 2 5 7 9"); + + imap_check + .send("UID SEARCH $ SMALLER 1000 SUBJECT section") + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SEARCH 8"); + + imap_check.send("UID SEARCH RETURN (MIN MAX) NOT $").await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("MIN 2 MAX 9"); + + // Sort + imap_check + .send("UID SORT (REVERSE SUBJECT REVERSE DATE) UTF-8 FROM Nathaniel") + .await; + imap_check + .assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_equals("* SORT 6 4 1"); + + imap.send("UID SORT RETURN (COUNT ALL) (DATE SUBJECT) UTF-8 ALL") + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("COUNT 10 ALL 6,4:5,1,3,7:8,10,2,9"); +} diff --git a/tests/src/imap/store.rs b/tests/src/imap/store.rs new file mode 100644 index 00000000..ab1a734a --- /dev/null +++ b/tests/src/imap/store.rs @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::ResponseType; + +use super::{AssertResult, ImapConnection, Type}; + +pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { + // Select INBOX + imap.send("SELECT INBOX").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("10 EXISTS") + .assert_contains("[UIDNEXT 11]"); + + // Set all messages to flag "Seen" + imap.send("UID STORE 1:10 +FLAGS.SILENT (\\Seen)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("FLAGS", 0); + + // Check that the flags were set + imap.send("UID FETCH 1:* (Flags)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("\\Seen", 10); + + // Check status + imap.send("STATUS INBOX (UIDNEXT MESSAGES UNSEEN)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("MESSAGES 10") + .assert_contains("UNSEEN 0") + .assert_contains("UIDNEXT 11"); + + // Remove Seen flag from all messages + imap.send("UID STORE 1:10 -FLAGS (\\Seen)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("FLAGS", 10) + .assert_count("Seen", 0); + + // Store using saved searches + imap.send("SEARCH RETURN (SAVE) FROM nathaniel").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("UID STORE $ +FLAGS (\\Answered)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("FLAGS", 3); + + // Remove Answered flag + imap.send("UID STORE 1:* -FLAGS (\\Answered)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("FLAGS", 10) + .assert_count("Answered", 0); +} diff --git a/tests/src/imap/thread.rs b/tests/src/imap/thread.rs new file mode 100644 index 00000000..5b1339e8 --- /dev/null +++ b/tests/src/imap/thread.rs @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020-2022, Stalwart Labs Ltd. + * + * This file is part of the Stalwart IMAP Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use imap_proto::ResponseType; + +use crate::imap::AssertResult; + +use super::{append::build_messages, ImapConnection, Type}; + +pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { + // Create test messages + let messages = build_messages(); + + // Insert messages using Multiappend + imap.send("CREATE Manchego").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + for (pos, message) in messages.iter().enumerate() { + if pos == 0 { + imap.send(&format!("APPEND Manchego {{{}}}", message.len())) + .await; + } else { + imap.send_untagged(&format!(" {{{}}}", message.len())).await; + } + imap.assert_read(Type::Continuation, ResponseType::Ok).await; + if pos < messages.len() - 1 { + imap.send_raw(message).await; + } else { + imap.send_untagged(message).await; + assert_eq!( + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .into_append_uid(), + format!("1:{}", messages.len()), + ); + } + } + + // Obtain ThreadId and MessageId of the first message + imap.send("SELECT Manchego").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + + let mut email_id = None; + let mut thread_id = None; + imap.send("UID FETCH 1 (EMAILID THREADID)").await; + for line in imap.assert_read(Type::Tagged, ResponseType::Ok).await { + if let Some((_, value)) = line.split_once("EMAILID (") { + email_id = value + .split_once(')') + .expect("Missing delimiter") + .0 + .to_string() + .into(); + } + if let Some((_, value)) = line.split_once("THREADID (") { + thread_id = value + .split_once(')') + .expect("Missing delimiter") + .0 + .to_string() + .into(); + } + } + let email_id = email_id.expect("Missing EMAILID"); + let thread_id = thread_id.expect("Missing THREADID"); + + // 4 different threads are expected + imap.send("THREAD REFERENCES UTF-8 1:*").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("(1 2 3 4)") + .assert_contains("(5 6 7 8)") + .assert_contains("(9 10 11 12)"); + + imap.send("THREAD REFERENCES UTF-8 SUBJECT T1").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("(5 6 7 8)") + .assert_count("(1 2 3 4)", 0) + .assert_count("(9 10 11 12)", 0); + + // Filter by threadId and messageId + imap.send(&format!( + "UID THREAD REFERENCES UTF-8 THREADID {}", + thread_id + )) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("(1 2 3 4)") + .assert_count("(", 1); + + imap.send(&format!("UID THREAD REFERENCES UTF-8 EMAILID {}", email_id)) + .await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_contains("(1)") + .assert_count("(", 1); + + // Delete all messages + imap.send("STORE 1:* +FLAGS.SILENT (\\Deleted)").await; + imap.assert_read(Type::Tagged, ResponseType::Ok).await; + imap.send("EXPUNGE").await; + imap.assert_read(Type::Tagged, ResponseType::Ok) + .await + .assert_count("EXPUNGE", 13); +} diff --git a/tests/src/jmap/auth_acl.rs b/tests/src/jmap/auth_acl.rs index 54fe200d..c7402d48 100644 --- a/tests/src/jmap/auth_acl.rs +++ b/tests/src/jmap/auth_acl.rs @@ -655,7 +655,7 @@ pub async fn test(server: Arc, admin_client: &mut Client) { for id in [jane_id.id(), john_id.id()] { add_user_id_to_group_id(directory, id as u32, sales_id.id() as u32).await; } - server.access_tokens.lock().clear(); + server.access_tokens.clear(); john_client.refresh_session().await.unwrap(); jane_client.refresh_session().await.unwrap(); bill_client.refresh_session().await.unwrap(); @@ -750,7 +750,7 @@ pub async fn test(server: Arc, admin_client: &mut Client) { // Remove John from the sales group remove_from_group(directory, john_id.id() as u32, sales_id.id() as u32).await; - server.sessions.lock().clear(); + server.sessions.clear(); assert_forbidden( john_client .set_default_account_id(&sales_id.to_string()) diff --git a/tests/src/jmap/auth_limits.rs b/tests/src/jmap/auth_limits.rs index 4216c419..449c29e0 100644 --- a/tests/src/jmap/auth_limits.rs +++ b/tests/src/jmap/auth_limits.rs @@ -53,8 +53,8 @@ pub async fn test(server: Arc, admin_client: &mut Client) { .await; // Reset rate limiters - server.rate_limit_auth.lock().clear(); - server.rate_limit_unauth.lock().clear(); + server.rate_limit_auth.clear(); + server.rate_limit_unauth.clear(); // Incorrect passwords should be rejected with a 401 error assert!(matches!( diff --git a/tests/src/smtp/outbound/mod.rs b/tests/src/smtp/outbound/mod.rs index eac2c0c3..4c7ace86 100644 --- a/tests/src/smtp/outbound/mod.rs +++ b/tests/src/smtp/outbound/mod.rs @@ -88,7 +88,9 @@ pub fn start_test_server(core: Arc, protocols: &[ServerProtocol]) -> watch server.spawn(smtp_manager.clone(), shutdown_rx) } ServerProtocol::Http => server.spawn(smtp_admin_manager.clone(), shutdown_rx), - ServerProtocol::Imap | ServerProtocol::Jmap => unreachable!(), + ServerProtocol::Imap | ServerProtocol::Jmap | ServerProtocol::ManageSieve => { + unreachable!() + } }; }) }