From 3da82580bc1886b1658003e4eb1435cd1ad287a2 Mon Sep 17 00:00:00 2001 From: Kibet-mutai Date: Fri, 29 Mar 2024 23:37:27 +0300 Subject: [PATCH] smtp client --- smtp/Cargo.lock | 283 +++++++++++++++++++++++++++++++++++++++++++ smtp/Cargo.toml | 9 ++ smtp/smtp_dialog.doc | 47 +++++++ smtp/src/main.rs | 144 ++++++++++++++++++++++ 4 files changed, 483 insertions(+) create mode 100644 smtp/Cargo.lock create mode 100644 smtp/Cargo.toml create mode 100644 smtp/smtp_dialog.doc create mode 100644 smtp/src/main.rs diff --git a/smtp/Cargo.lock b/smtp/Cargo.lock new file mode 100644 index 0000000..65ebf78 --- /dev/null +++ b/smtp/Cargo.lock @@ -0,0 +1,283 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "smtp" +version = "0.1.0" +dependencies = [ + "chrono", +] + +[[package]] +name = "syn" +version = "2.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/smtp/Cargo.toml b/smtp/Cargo.toml new file mode 100644 index 0000000..9435ded --- /dev/null +++ b/smtp/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "smtp" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4.37" diff --git a/smtp/smtp_dialog.doc b/smtp/smtp_dialog.doc new file mode 100644 index 0000000..552c793 --- /dev/null +++ b/smtp/smtp_dialog.doc @@ -0,0 +1,47 @@ +In a typical session, SMTP dialog works as follows: + - The client first establishes connection to the SMTP server. + - The server initiates with a greeting. This greeting indicates that the server is ready to receive commands. + - The client then issues its own greeting. + - Server responds. + - the client sends a command indicating who the mail is from. + - The server responds to indicate that the sender is accepted. + - The client issues anaother command, which specifies the mail recipient. + - The server responds indicating the recipient is accepted. + - The client then issues a DATA command. + - The server responds asking the client to proceed. + - The client transfers the email. + +Example: + S: 220 mail.example.com SMTP server ready + C: HELO mail.example.net + S: 250 Hello mail.example.net [192.0.2.67] + C: MAIL FROM: + S: 250 OK + C: RCPT TO: + S: 250 Accepted + C: DATA + S: 354 Enter message, ending with "." on a line by itself + C: Subject: Re: The Cake + C: Date: Fri, 03 May 2019 02:31:20 +0000 + C: + C: Do NOT forget to bring the cake! + C: . + S: 250 OK + C: QUIT + S: 221 closing connection + +The common client commands we use are as follows: + HELO is used for the client to identify itself to the server. + MAIL is used to specify who is sending the mail. + RCPT is used to specify a recipient. + DATA is used to initiate the transfer of the actual email. This email + should include both headers and a body. + QUIT is used to end the session. + +The server response codes used in a successful email transfer are the +following: + 220: The service is ready + 250: The requested command was accepted and completed successfully + 354: Start sending the message + 221: The connection is closing +Error codes vary between providers, but they are generally in the 500 range. diff --git a/smtp/src/main.rs b/smtp/src/main.rs new file mode 100644 index 0000000..b2a8a2b --- /dev/null +++ b/smtp/src/main.rs @@ -0,0 +1,144 @@ +use std::{ fmt::Display, io::{stdin, Error, Read, Write}, net::{Ipv4Addr, SocketAddr, TcpStream}}; +use chrono::{DateTime, Utc}; +//struct Error { + // error: String, +//} +//impl Error { + // pub fn new(error: String) -> Self { + // Error { error } + //} +//} + +#[derive(Debug)] +enum Errors { + ParseResponseError, +} + +impl Display for Errors { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Errors::ParseResponseError => { + write!(f, "Invalid Response") + } + } + } +} + +fn main() { +let host = Ipv4Addr::new(142, 251, 9, 27); + let mut stream = connect_to_host(host, 25).unwrap(); + wait_on_response(220, &stream); + //needs refactoring + loop { + let input = String::new(); + let greetings = get_input(&input); + send_input(&stream, &greetings); + wait_on_response(250, &stream); + let sender = get_input(&input); + // println!("Sender email: {}", sender); + send_input(&stream, &sender); + wait_on_response(250, &stream); + let recepient = get_input(&input); + send_input(&stream, &recepient); + wait_on_response(250, &stream); + send_input(&stream, "DATA"); + wait_on_response(354, &stream); + let subject = get_input(&input); + let mut header_s = sender.split(":"); + header_s.next(); + let sender_h = header_s.next().unwrap(); + let h_s = format!("From:{}\r\n", sender_h); + let mut rcpt_s = recepient.split(":"); + rcpt_s.next(); + let rcpt_h = rcpt_s.next().unwrap(); + let r_s = format!("To:{}\r\n", rcpt_h); + send_input(&stream, &h_s); + // println!("Senders header: {}", h_s); + send_input(&stream, &recepient); + // println!("Recepients header: {}", r_s); + send_input(&stream, &subject); + format!("Subject:{}\r\n", subject); + let now: DateTime = Utc::now(); + let date = now.to_rfc2822(); + format!("Date:{}\r\n", date); + send_input(&stream, &date); + send_input(&stream, "\r\n"); + loop { + let body = get_input(&input); + send_input(&stream, &body); + println!("BODY: {}", body); + if body.starts_with(".") { + break; + } + } + wait_on_response(250, &stream); + send_input(&stream, "QUIT"); + wait_on_response(221, &stream); + } + +} + +// Sample Responses +/*response 1*/ +//250 Message received! +/*response 2*/ +//250-Message +//250 received! + +fn send_input(mut stream: &TcpStream, command: &str) { + let formated_str = format!("{}\r\n", command); + stream.write(formated_str.as_bytes()).unwrap(); +} + + +fn get_input(input: &str) -> String { + let mut s = input.to_string(); + stdin().read_line(&mut s).expect("Invalid Request"); + if let Ok(s_str) = Ok::<&str, Box>(s.trim()) { + s_str.to_string() + } else { + format!("Error: Invalid user input!") + } +} + +fn parse_response(response: &str) -> Option { + for i in response.split("\r\n") { + if i.len() < 4 { + continue; + } + let (digits, rest) = i.split_at(3); + if digits.chars().all(char::is_numeric) && rest.starts_with('-') || rest.starts_with(' ') { + return digits.parse::().ok(); + } + } + + None +} + +fn wait_on_response(expected: i32, mut stream: &TcpStream) { + let mut buffer = [0u8; 1024]; + let bytes_received = stream.read(&mut buffer).unwrap(); + if bytes_received < 1 { + println!("Connection dropped!"); + std::process::exit(1); + } + let res_code = parse_response(std::str::from_utf8(&buffer).unwrap()).unwrap(); + if res_code != expected { + println!("Server Error: {res_code}"); + std::process::exit(1); + } + + let response = std::str::from_utf8(&buffer).unwrap(); + let res = response.to_string(); + let mut first_str = res.split("\r\n"); + let last = first_str.next().unwrap(); + + println!("S: {}",last); +} + + +fn connect_to_host(host: Ipv4Addr, port: u16) -> Result { + let addr = SocketAddr::new(std::net::IpAddr::V4(host), port); + let stream = TcpStream::connect(addr)?; + Ok(stream) +}