Skip to content

Add outlook_oauth2 example #288

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,014 changes: 999 additions & 15 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ ouroboros = "0.18.0"
lettre = "0.11"
rustls-connector = "0.19.0"
clap = { version = "4.5.4", features = ["derive"] }
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
serde = { version = "1.0.200", features = ["derive"] }
axum = "0.7.5"
axum-server = { version = "0.6", features = ["tls-rustls"] }
oauth2 = { version = "4.4.2", features = [] }
dotenvy = "0.15.7"

# to make -Zminimal-versions work
[target.'cfg(any())'.dependencies]
Expand Down Expand Up @@ -67,6 +73,10 @@ required-features = ["default"]
name = "timeout"
required-features = ["default"]

[[example]]
name = "outlook_oauth2"
required-features = ["default"]

[[test]]
name = "imap_integration"
required-features = ["default"]
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This directory contains examples of working with the IMAP client.
Examples:
* basic - This is a very basic example of using the client.
* gmail_oauth2 - This is an example using oauth2 for logging into gmail as a secure appplication.
* outlook_oauth2 - Similar to gmail example, but for outlook.
* idle - This is an example showing how to use IDLE to monitor a mailbox.
* rustls - This demonstrates how to use Rustls instead of Openssl for secure connections (helpful for cross compilation).
* starttls - This is an example showing how to use STARTTLS after connecting over plaintext.
Expand Down
4 changes: 4 additions & 0 deletions examples/outlook_oauth2/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AZURE_OAUTH_APP_TENANT_ID=4******8-cc46-4d4b-83b2-f************a # or 'consumers' (see README.md)
AZURE_OAUTH_APP_CLIENT_ID=f******8-1914-40fa-baa6-8************2
AZURE_OAUTH_APP_CLIENT_SECRET=fe***************************************hcvr
[email protected] # or personal address (see README.md)
1 change: 1 addition & 0 deletions examples/outlook_oauth2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
9 changes: 9 additions & 0 deletions examples/outlook_oauth2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Prerequisites

To run this example, you will need a registered single-tenant OAuth application at `Azure`. Follow [this guide](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth) if you do not have one just yet. Note that the example was created against a single-tenant OAuth app (i.e. for email addresses belonging to your organisation), so make sure to select an appropriate option when registering your application. Alternatively, you can create an app for personal microsoft accounts, but then make sure to specify `AZURE_OAUTH_APP_TENANT_ID=consumers` and `EMAIL_ADDRESS=<[email protected]>` in your `.env` file (read further).

Important! When registering your app (or in the app's settings afterwards), specify `https://localhost:3993` as a redirect url and API permissions for the app should be at least `IMAP.AccessAsUser.All`. Also make sure to select `ID tokens (used for implicit and hybrid flows)` in the app's authentication settings.

Create a `.env` file in this example's directory filling in the required data (`.env.sample` has got an exhaustive list of variables needed for this example to work).

You can now hit `cargo run --example outlook_oauth2` from the repo's root and visit `https://localhost:3993` in your browser. You will want to tell your browser to _dangerously_ trust the self-signed certificates for this domain, but this is _only_ for testing / demo purposes.
21 changes: 21 additions & 0 deletions examples/outlook_oauth2/certs/imap.local.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDazCCAlMCFAxQwXkfT4M84/fevISct//qQskRMA0GCSqGSIb3DQEBCwUAMHEx
CzAJBgNVBAYTAlVaMREwDwYDVQQIDAhUYXNoa2VudDERMA8GA1UEBwwIVGFzaGtl
bnQxEzARBgNVBAoMCmZha3RvcnktcnMxEzARBgNVBAsMCmZha3RvcnktcnMxEjAQ
BgNVBAMMCWxvY2FsaG9zdDAgFw0yNDAyMDMyMDI1MDlaGA8zMDA0MDQwNjIwMjUw
OVowcTELMAkGA1UEBhMCVVoxETAPBgNVBAgMCFRhc2hrZW50MREwDwYDVQQHDAhU
YXNoa2VudDETMBEGA1UECgwKZmFrdG9yeS1yczETMBEGA1UECwwKZmFrdG9yeS1y
czESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA4ektheqTRy+eHn9j22AxGHqtg/elEiZC0UCLX51ysEkhnLLvFlVFtzd7
q+nx1PNiHdH5i/TjdAYrXAZhKU/k2YfrgCyOjm/XxSw7ujXPP+cWOmdRYTexT9o7
Yrg3ZYMniJbbTl8j37dieXHaO7FHAvpww1q/nbQkwD/1WqK1ggQY/OZ38wpUvsws
9LA7shuXdGnjAXunnRGEzZ2EG6T5hYw0PFL+2CHwr0lqNbCur8wu99t4ED9/vfLG
0TWRQwSnApyjHy89rn5Ze3vOiNzcBW778oZxwvzriEmbQQg6RxKE19AlaiV4+n5S
woAi8Ji69BKRUSlxRhW6eX4ABV2eOwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQDS
EXuIvVx27LyWlIhfY6vwSWqeUoRXmMFpiBNTTvvHQKlJzLlDyn1b+CqHvMdE9RZh
FI5shZkiqtRRTUGVHB4o0ntwCQmWyV/5FQQ6EYs/bHXUcN2vt1XuU7WK4fRafPPu
snYDgg0TmpGvm+J8W64TfJogWqpPsnT4pOF+aNqW88TTs1JUnNFDBQmw2QKBK+AH
+V4zhpCjVXpKtVMTnDWHQfJh4whelD18lU1jPCbzQrRs2hQWQvtzKWi0YCYc1IXl
4E6eIOHRuiUl/mE3p3f2CGJIwxgrMuxN07ncnwVXBPCaVzSLWJHy0G61mFKH5R/7
42EC7S/POk5GtzkMJ5Du
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions examples/outlook_oauth2/certs/imap.local.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6S2F6pNHL54e
f2PbYDEYeq2D96USJkLRQItfnXKwSSGcsu8WVUW3N3ur6fHU82Id0fmL9ON0Bitc
BmEpT+TZh+uALI6Ob9fFLDu6Nc8/5xY6Z1FhN7FP2jtiuDdlgyeIlttOXyPft2J5
cdo7sUcC+nDDWr+dtCTAP/VaorWCBBj85nfzClS+zCz0sDuyG5d0aeMBe6edEYTN
nYQbpPmFjDQ8Uv7YIfCvSWo1sK6vzC7323gQP3+98sbRNZFDBKcCnKMfLz2ufll7
e86I3NwFbvvyhnHC/OuISZtBCDpHEoTX0CVqJXj6flLCgCLwmLr0EpFRKXFGFbp5
fgAFXZ47AgMBAAECggEAJjyV4G86O1fDbw0HxUdMOAT3nnkJfv9r2sgObwISueS+
5CtjDUgkkyS4cXoY3P7O0hZKoxYxc19h8mMACgKETQ9U3G5uOIyUnEJm35cg+4Ns
/ziijQ5knAvndkeQ1MU0qUlDWEoBI+oBqGWNVwIj70ydTmtrOFGX0NRiflNA3n7q
pJbdRZzKnTxXxRwIRuGA1y6SlBLQ740hVOm56iLtRJ+P0kNErSL8Uhws/X9/0MXH
W8r2JVikNumBZH18MK+wBGulwZBcLurFfv31hbeQ/FnckOJ1OE53rnV+tBrZN7Ap
6eR4IMcVPfunnGX+meEUnJfmC0HrdQXucDB8Ey/biQKBgQDygP0JeUKpSWX2uSfV
2c8N0opmC2uHswOhf+H9TOyA4DO5NmlbOqVv+uUwRQvIkoen8XNMCPOyoK7WZNAB
hfyU+ck3HDIBqHbGBisUXDNLgIQIhWVznYK0QC+YYr+rEmFun0sMriuhZsU1q2mW
VoAPSTJhaufRb0TKib9Tarzg4wKBgQDue8jk0tbK5xL9dcyn1CxHtDAbfyQfQnSd
G+GcQDDCamgbKI042A5lPSToYEOpSMTOn/n5CmezsSMFnwuwZAgQ1Pbd3YeknBCi
6jWzqYcC11u3EeX9YPJgEDZq0uSWNZg0phDBsu+PYq7vDAriCsMeQrLMvQb0Fs3n
Pp4vVzSEyQKBgQCb+h1G/6jBzAT6WYNmyE6mPFpqYkQKpzjZorCPxO+FwS9jnLzN
Qf5w9TZ/Apoeqyj3+5RGPqfIqBNssLEdmbmpdLRYbxk2+c1Td1o0IU2Y7ZN/C5YC
dDhCidpTMIjJluv2RBz4jfpgOQL1j0g9u2to6ZKvGBz9F41unITkOY49MwKBgEzk
1qqJHL6BcQsOT3WRoNFh1N0YyoHVwJnjooPp4o7dFkIjeh1o9INKCrtuRoKvtt1U
kZnt8+/pXnxygqdWKY+byxlQU2sM8wREdho+wAx3edf2Smy/NIcq0xDwfMm98ByR
qvd5hWp7DCKBhITLqYv5P4NqM3LCY5N7CjADcyiZAoGBALXXR5WSHLjtzaN4Eeti
pWur1VN30HiM2zRTXwTxx6X7y/FI5xzoCVAJb6tSpC/aXzFx05Xa/LyhDXI2sbhm
G3a4tjBRrief5z8XQ7gdBSiyRtLc1XFy3kmeN2HTPMWSIrbk56xyEOqbXov5S+41
hWwNT3lodEZ2ymFWEZHHAvhb
-----END PRIVATE KEY-----
135 changes: 135 additions & 0 deletions examples/outlook_oauth2/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use axum::extract::{Query, State};
use axum::response::Html;
use axum::routing::get;
use axum::Router;
use axum_server::tls_rustls::RustlsConfig;
use oauth2::AccessToken;
use serde::Deserialize;
use std::sync::Arc;
use std::{env, net::SocketAddr, path::PathBuf};

mod state;
mod utils;

struct OutlookOAuth2 {
user: String,
access_token: AccessToken,
}

impl imap::Authenticator for OutlookOAuth2 {
type Response = String;
#[allow(unused_variables)]
fn process(&self, data: &[u8]) -> Self::Response {
format!(
"user={}\x01auth=Bearer {}\x01\x01",
self.user,
self.access_token.secret()
)
}
}

#[tokio::main]
async fn main() {
// load environment variables
let this_example_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join("outlook_oauth2");
dotenvy::from_path(this_example_dir.join(".env")).ok();

// load self-signed certificates
let certs_dir = this_example_dir.join("certs");
let config = RustlsConfig::from_pem_file(
certs_dir.join("imap.local.crt"),
certs_dir.join("imap.local.key"),
)
.await
.unwrap();

// define application and ...
let state = Arc::new(state::AppState::default());
let app = Router::new().route("/", get(home)).with_state(state);

// ... launch it
axum_server::bind_rustls(SocketAddr::from(([127, 0, 0, 1], 3993)), config)
.serve(app.into_make_service())
.await
.unwrap();
}

#[derive(Deserialize)]
struct AuthCode {
code: String,
state: String,
}

async fn home(
auth_code: Option<Query<AuthCode>>,
State(state): State<Arc<state::AppState>>,
) -> Html<String> {
// already redirected back to homepage by the authority
// with a code in query string parameters
if let Some(auth_code) = auth_code {
assert!(!auth_code.state.is_empty());
// exchange the code they brought from the authority for an access token
let access_token =
utils::exchange_code_for_token(&state.oauth_client, auth_code.code.clone()).await;
// instantiate an authenticator
let outlook_oauth = OutlookOAuth2 {
user: env::var("EMAIL_ADDRESS").unwrap(),
access_token,
};
// establish a connection to the IMAP server
let client = imap::ClientBuilder::new("outlook.office365.com", 993)
.connect()
.expect("successfly connected");
// start a session
let mut session = client
.authenticate("XOAUTH2", &outlook_oauth)
.expect("authenticated connection");
// fetch the first email in the inbox
session.select("INBOX").unwrap();
let messages = session.fetch("1", "RFC822").unwrap();
let first_message = messages.iter().next().unwrap().body().unwrap();
let first_message = std::str::from_utf8(first_message).unwrap();
// end the session
session.logout().unwrap();
// render the contents of the fetched email
let page = format!(
r#"
<!doctype html>
<html lang="en">
<head>
<title>Home</title>
<meta name="viewport" content="width=device-width">
</head>
<body>
<span>First message in your inbox</span>
<span>{:?}</span>
</body>
</html>
"#,
first_message,
);
return Html(page);
}
// should go and log in to their microsoft account (if not already logged in)
// and explicitly authorize the OAuth application, after which the authority
// will issue a code that can be later on echanged for an access token
let url = utils::build_auth_url(&state.oauth_client);
let page = format!(
r#"
<!doctype html>
<html lang="en">
<head>
<title>Home</title>
<meta name="viewport" content="width=device-width">
</head>
<body>
<a href="{}">Show my fist letter</a>
</body>
</html>
"#,
url.to_string()
);
Html(page)
}
26 changes: 26 additions & 0 deletions examples/outlook_oauth2/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use oauth2::basic::{BasicErrorResponseType, BasicTokenType};
use oauth2::{
Client, EmptyExtraTokenFields, RevocationErrorResponseType, StandardErrorResponse,
StandardRevocableToken, StandardTokenIntrospectionResponse, StandardTokenResponse,
};

pub(crate) type OauthClient = Client<
StandardErrorResponse<BasicErrorResponseType>,
StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
BasicTokenType,
StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>,
StandardRevocableToken,
StandardErrorResponse<RevocationErrorResponseType>,
>;

pub(crate) struct AppState {
pub(crate) oauth_client: OauthClient,
}

impl Default for AppState {
fn default() -> Self {
Self {
oauth_client: super::utils::build_oauth_client(),
}
}
}
51 changes: 51 additions & 0 deletions examples/outlook_oauth2/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use oauth2::basic::BasicClient;
use oauth2::reqwest::async_http_client;
use oauth2::url::Url;
use oauth2::AccessToken;
use oauth2::TokenResponse;
use oauth2::{
AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, TokenUrl,
};
use std::env;

pub(crate) fn build_auth_url(client: &super::state::OauthClient) -> Url {
let (url, _csrf_token) = client
.authorize_url(CsrfToken::new_random)
.add_scope(Scope::new(
"https://outlook.office.com/IMAP.AccessAsUser.All".to_string(),
))
.url();
url
}

pub(crate) fn build_oauth_client() -> super::state::OauthClient {
let client_id = ClientId::new(env::var("AZURE_OAUTH_APP_CLIENT_ID").unwrap());
let client_secret = ClientSecret::new(env::var("AZURE_OAUTH_APP_CLIENT_SECRET").unwrap());
let tenant_id = env::var("AZURE_OAUTH_APP_TENANT_ID").unwrap();
let auth_url = AuthUrl::new(format!(
"https://login.microsoftonline.com/{}/oauth2/v2.0/authorize",
tenant_id
))
.expect("valid url");
let token_url = TokenUrl::new(format!(
"https://login.microsoftonline.com/{}/oauth2/v2.0/token",
tenant_id
))
.expect("valid url");
BasicClient::new(client_id, Some(client_secret), auth_url, Some(token_url)).set_redirect_uri(
RedirectUrl::new("https://localhost:3993".to_string()).expect("valid url"),
)
}

pub(crate) async fn exchange_code_for_token(
client: &super::state::OauthClient,
code: String,
) -> AccessToken {
client
.exchange_code(AuthorizationCode::new(code))
.request_async(async_http_client)
.await
.expect("response with token")
.access_token()
.to_owned()
}