diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5b10d63 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3" + +services: + mail: + image: greenmail/standalone + ports: + - "3025:3025" # SMTP + - "3143:3143" # IMAP + - "3465:3465" # SMTPS + - "3993:3993" # IMAPS + - "3080:8080" # Web + environment: + - "GREENMAIL_OPTS=-Dgreenmail.setup.test.all -Dgreenmail.users=user:pass -Dgreenmail.verbose" diff --git a/imap/Cargo.toml b/imap/Cargo.toml index 20f39a0..dabf0a7 100644 --- a/imap/Cargo.toml +++ b/imap/Cargo.toml @@ -3,7 +3,12 @@ name = "imap" version = "0.1.0" edition = "2018" +[[bin]] +name = "greenmail-test" +path = "bin/greenmail_test.rs" + [features] +default = ["rfc2177-idle"] rfc2177-idle = [] [dependencies] diff --git a/imap/bin/greenmail_test.rs b/imap/bin/greenmail_test.rs new file mode 100644 index 0000000..97c30fc --- /dev/null +++ b/imap/bin/greenmail_test.rs @@ -0,0 +1,4 @@ +#[tokio::main] +async fn main() { + +} \ No newline at end of file diff --git a/imap/src/client/auth.rs b/imap/src/client/auth.rs index c45e15c..7127603 100644 --- a/imap/src/client/auth.rs +++ b/imap/src/client/auth.rs @@ -1,8 +1,33 @@ -pub trait AuthMethod {} +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::proto::command::{Command, CommandLogin}; +use crate::client::inner::Inner; + +pub trait Client: AsyncRead + AsyncWrite + Unpin + Sync + Send + 'static {} + +#[async_trait] +pub trait AuthMethod { + async fn perform_auth(&self, inner: &mut Inner) + where + C: Client; +} pub struct Login { pub username: String, pub password: String, } -impl AuthMethod for Login {} +#[async_trait] +impl AuthMethod for Login { + async fn perform_auth(&self, inner: &mut Inner) + where + C: Client, + { + let command = Command::Login(CommandLogin { + username: &self.username, + password: &self.password, + }); + + let result = inner.execute(command).await; + } +} diff --git a/imap/src/client/inner.rs b/imap/src/client/inner.rs index 11096a6..e0fddf9 100644 --- a/imap/src/client/inner.rs +++ b/imap/src/client/inner.rs @@ -1,13 +1,17 @@ use anyhow::Result; use futures::future::FutureExt; use tokio::{ - io::{split, AsyncRead, AsyncWrite, ReadHalf}, + io::{split, AsyncRead, AsyncWrite, ReadHalf, WriteHalf}, sync::oneshot, task::JoinHandle, }; +use tokio_rustls::client::TlsStream; use tokio_util::codec::FramedRead; use crate::codec::ImapCodec; +use crate::proto::command::Command; + +use super::upgrade::upgrade; type ExitSender = oneshot::Sender<()>; type ExitListener = oneshot::Receiver<()>; @@ -15,11 +19,13 @@ type ExitListener = oneshot::Receiver<()>; pub struct Inner { read_exit: ExitSender, read_handle: JoinHandle>, + + write_half: WriteHalf, } impl Inner where - C: AsyncRead + AsyncWrite + Send + 'static, + C: AsyncRead + AsyncWrite + Unpin + Send + 'static, { pub async fn open(c: C) -> Result { // break the stream of bytes into a reader and a writer @@ -34,8 +40,25 @@ where Ok(Inner { read_exit, read_handle, + write_half, }) } + + pub async fn execute<'a>(&mut self, command: Command<'a>) {} + + pub async fn upgrade(self) -> Result>> { + // TODO: check that this capability exists?? + // issue exit to the read loop and retrieve the read half + let _ = self.read_exit.send(()); + let read_half = self.read_handle.await?; + let write_half = self.write_half; + + // put the read half and write half back together + let stream = read_half.unsplit(write_half); + let tls_stream = upgrade(stream, "hellosu").await?; + + Inner::open(tls_stream).await + } } // exit is a channel that will notify this loop when some external @@ -57,5 +80,6 @@ where _ = exit => break, } } - todo!() + + framed.into_inner() } diff --git a/imap/src/client/upgrade.rs b/imap/src/client/upgrade.rs index 790d6e5..d290463 100644 --- a/imap/src/client/upgrade.rs +++ b/imap/src/client/upgrade.rs @@ -1,4 +1,24 @@ -use anyhow::Result; -use tokio_rustls::TlsStream; +use std::sync::Arc; -pub fn upgrade(c: C) -> Result> { todo!() } +use anyhow::Result; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_rustls::{ + client::TlsStream, rustls::ClientConfig as RustlsConfig, webpki::DNSNameRef, TlsConnector, +}; + +pub async fn upgrade(c: C, hostname: impl AsRef) -> Result> +where + C: AsyncRead + AsyncWrite + Unpin, +{ + let server_name = hostname.as_ref(); + + let mut tls_config = RustlsConfig::new(); + tls_config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + let tls_config = TlsConnector::from(Arc::new(tls_config)); + let dnsname = DNSNameRef::try_from_ascii_str(server_name).unwrap(); + let stream = tls_config.connect(dnsname, c).await?; + + Ok(stream) +} diff --git a/imap/src/proto/command.rs b/imap/src/proto/command.rs index 1edbfa1..984b2b5 100644 --- a/imap/src/proto/command.rs +++ b/imap/src/proto/command.rs @@ -1,3 +1,43 @@ pub enum Command<'a> { - Todo(&'a ()), + // Any state + Capability, + Noop, + Logout, + + // Not authenticated + Login(CommandLogin<'a>), + Starttls, + Authenticate, + + // Authenticated + Select, + Examine, + Create, + Delete, + Rename, + Subscribe, + Unsubscribe, + List, + Lsub, + Status, + Append, + + // Selected + Check, + Close, + Expunge, + Search, + Fetch, + Store, + Copy, + Uid, + + // Extensions + #[cfg(feature = "rfc2177-idle")] + Idle, } + +pub struct CommandLogin<'a> { + pub username: &'a str, + pub password: &'a str, +} \ No newline at end of file diff --git a/imap/src/proto/mod.rs b/imap/src/proto/mod.rs index 89ca9c5..e70cbce 100644 --- a/imap/src/proto/mod.rs +++ b/imap/src/proto/mod.rs @@ -11,3 +11,6 @@ pub mod response; pub mod parsers; pub mod rfc2234; pub mod rfc3501; + +#[cfg(feature = "rfc2177-idle")] +pub mod rfc2177; \ No newline at end of file diff --git a/imap/src/proto/rfc2177.rs b/imap/src/proto/rfc2177.rs new file mode 100644 index 0000000..0939bb2 --- /dev/null +++ b/imap/src/proto/rfc2177.rs @@ -0,0 +1 @@ +//! Grammar from https://datatracker.ietf.org/doc/html/rfc2177#section-4 diff --git a/imap/src/proto/rfc3501.rs b/imap/src/proto/rfc3501.rs index 76e6f8f..ef5f0bb 100644 --- a/imap/src/proto/rfc3501.rs +++ b/imap/src/proto/rfc3501.rs @@ -58,8 +58,11 @@ rule!(pub capability_data : Vec => preceded(tag_no_case("CAPABILITY" pub(crate) fn is_list_wildcards(c: u8) -> bool { c == b'%' || c == b'*' } rule!(pub list_wildcards : u8 => satisfy(is_list_wildcards)); -/// literal = "{" number "}" CRLF *CHAR8 -/// ; Number represents the number of CHAR8s +// literal = "{" number "}" CRLF *CHAR8 +// ; Number represents the number of CHAR8s +// TODO: Future work, could possibly initialize writing to file if the length is +// determined to exceed a certain threshold so we don't have insane amounts of +// data in memory pub fn literal(i: &[u8]) -> IResult<&[u8], Vec> { let mut length_of = terminated(delimited(char('{'), number, char('}')), CRLF); let (i, length) = length_of(i)?;