diff --git a/Cargo.lock b/Cargo.lock index dc8e1a0..a301f67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -968,9 +968,12 @@ dependencies = [ "dashmap", "directories", "dotenv", + "hyper", + "hyper-tls", "rand 0.8.5", "rocket", "sdk", + "serde_urlencoded", "toml 0.7.2", ] @@ -1658,9 +1661,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.24" +version = "0.14.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" dependencies = [ "bytes", "futures-channel", diff --git a/README.md b/README.md index 16e95f7..cf6bae8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@
&&&")]
+pub async fn handle_redirect(
+ code: Option,
+ state: Json,
+ scope: Option,
+ error: Option,
+ config: &State,
+ http_client: &State,
+) -> ResponseResult {
+ if code.is_some() && scope.is_some() {
+ let provider = match config.oauth2().providers().get(state.provider()) {
+ Some(provider) => provider,
+ None => {
+ return Err(ErrResponse::new(
+ ErrorKind::BadRequest,
+ "Could not find requested oauth provider",
+ ))
+ }
+ };
+
+ let redirect_uri = format!("{}/mail/oauth2/redirect", config.external_host());
+ let token_url = provider.token_url();
+ let secret_token = provider.secret_token();
+ let public_token = provider.public_token();
+ let code = code.unwrap();
+
+ let access_token_response = get_access_token(
+ &http_client,
+ token_url,
+ code.as_str(),
+ redirect_uri.as_str(),
+ public_token,
+ secret_token,
+ )
+ .await
+ .map_err(|err| ErrResponse::from(err).into())?;
+
+ println!("{}", access_token_response.access_token());
+
+ Ok(OkResponse::new(token_url.to_string()))
+ } else if error.is_some() {
+ Err(ErrResponse::new(ErrorKind::BadRequest, "yeet"))
+ } else {
+ Err(ErrResponse::new(
+ ErrorKind::BadRequest,
+ "Missing required params",
+ ))
+ }
+}
diff --git a/apps/server/src/routes/mail/oauth2/tokens.rs b/apps/server/src/routes/mail/oauth2/tokens.rs
new file mode 100644
index 0000000..8200d44
--- /dev/null
+++ b/apps/server/src/routes/mail/oauth2/tokens.rs
@@ -0,0 +1,20 @@
+use std::collections::HashMap;
+
+use rocket::State;
+
+use crate::{
+ state::Config,
+ types::{OkResponse, ResponseResult},
+};
+
+#[get("/tokens")]
+pub fn get_tokens(config: &State) -> ResponseResult> {
+ let public_tokens: HashMap = config
+ .oauth2()
+ .providers()
+ .iter()
+ .map(|(key, value)| return (key.to_string(), value.public_token().to_string()))
+ .collect();
+
+ Ok(OkResponse::new(public_tokens))
+}
diff --git a/apps/server/src/state/config/mod.rs b/apps/server/src/state/config/mod.rs
index d18f8ac..49e3204 100644
--- a/apps/server/src/state/config/mod.rs
+++ b/apps/server/src/state/config/mod.rs
@@ -2,6 +2,7 @@ mod appearance;
mod authorization;
mod cache;
mod limit;
+mod oauth2;
use rocket::serde::{Deserialize, Serialize};
@@ -14,6 +15,8 @@ use limit::RateLimit;
pub use authorization::{default_expiry_time, AuthType};
+use self::oauth2::OAuth2;
+
#[derive(Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct Config {
@@ -23,12 +26,15 @@ pub struct Config {
host: String,
#[serde(default = "behind_proxy")]
behind_proxy: bool,
+ external_host: String,
#[serde(default)]
rate_limit: RateLimit,
#[serde(default)]
appearance: Appearance,
#[serde(default)]
cache: Cache,
+ #[serde(default)]
+ oauth2: OAuth2,
auth: Option,
}
@@ -45,6 +51,10 @@ impl Config {
&self.behind_proxy
}
+ pub fn external_host(&self) -> &str {
+ &self.external_host
+ }
+
pub fn rate_limit(&self) -> &RateLimit {
&self.rate_limit
}
@@ -60,6 +70,10 @@ impl Config {
pub fn authorization(&self) -> Option<&Authorization> {
self.auth.as_ref()
}
+
+ pub fn oauth2(&self) -> &OAuth2 {
+ &self.oauth2
+ }
}
impl Default for Config {
@@ -72,10 +86,12 @@ impl Default for Config {
Self {
appearance: Appearance::default(),
+ external_host: String::from("https://example.com"),
auth,
rate_limit: RateLimit::default(),
cache: Cache::default(),
behind_proxy: behind_proxy(),
+ oauth2: OAuth2::default(),
host: default_host(),
port: default_port(),
}
diff --git a/apps/server/src/state/config/oauth2.rs b/apps/server/src/state/config/oauth2.rs
new file mode 100644
index 0000000..1fe157a
--- /dev/null
+++ b/apps/server/src/state/config/oauth2.rs
@@ -0,0 +1,45 @@
+use std::collections::HashMap;
+
+use rocket::serde::{Deserialize, Serialize};
+
+#[derive(Deserialize, Serialize)]
+#[serde(crate = "rocket::serde")]
+pub struct OAuth2 {
+ providers: HashMap,
+}
+
+impl OAuth2 {
+ pub fn providers(&self) -> &HashMap {
+ &self.providers
+ }
+}
+
+impl Default for OAuth2 {
+ fn default() -> Self {
+ Self {
+ providers: HashMap::new(),
+ }
+ }
+}
+
+#[derive(Deserialize, Serialize)]
+#[serde(crate = "rocket::serde")]
+pub struct Provider {
+ public_token: String,
+ secret_token: Option,
+ token_url: String,
+}
+
+impl Provider {
+ pub fn public_token(&self) -> &str {
+ &self.public_token
+ }
+
+ pub fn secret_token(&self) -> &Option {
+ &self.secret_token
+ }
+
+ pub fn token_url(&self) -> &str {
+ &self.token_url
+ }
+}
diff --git a/apps/server/src/types/error.rs b/apps/server/src/types/error.rs
index f67f280..a97f268 100644
--- a/apps/server/src/types/error.rs
+++ b/apps/server/src/types/error.rs
@@ -7,6 +7,8 @@ use sdk::types::Error as SdkError;
#[serde(crate = "rocket::serde")]
pub enum ErrorKind {
SdkError(SdkError),
+ CreateHttpRequest,
+ BadConfig,
Unauthorized,
BadRequest,
TooManyRequests,
diff --git a/apps/server/src/types/response.rs b/apps/server/src/types/response.rs
index e363cf6..1b92720 100644
--- a/apps/server/src/types/response.rs
+++ b/apps/server/src/types/response.rs
@@ -28,6 +28,8 @@ pub struct ErrResponse {
impl ErrResponse {
fn find_status_from_error_kind(error_kind: &ErrorKind) -> Status {
match error_kind {
+ ErrorKind::BadConfig => Status::InternalServerError,
+ ErrorKind::CreateHttpRequest => Status::InternalServerError,
ErrorKind::Unauthorized => Status::Unauthorized,
ErrorKind::BadRequest => Status::BadRequest,
ErrorKind::TooManyRequests => Status::TooManyRequests,
diff --git a/apps/web/src-tauri/src/commands.rs b/apps/web/src-tauri/src/commands.rs
index 47b99b6..1580a8b 100644
--- a/apps/web/src-tauri/src/commands.rs
+++ b/apps/web/src-tauri/src/commands.rs
@@ -1,10 +1,13 @@
use sdk::{
detect::{self, Config},
- session::Credentials,
+ session::FullLoginOptions,
types::{MailBox, Message, Preview},
};
-use crate::{types::{session, Result, Sessions}, files::CacheFile};
+use crate::{
+ files::CacheFile,
+ types::{session, Result, Sessions},
+};
use tauri::{async_runtime::spawn_blocking, State};
@@ -15,7 +18,7 @@ pub async fn detect_config(email_address: String) -> Result {
#[tauri::command(async)]
pub async fn login(
- credentials: Credentials,
+ credentials: FullLoginOptions,
session_handler: State<'_, Sessions>,
) -> Result {
// Connect and login to the mail servers using the user provided credentials.
@@ -39,9 +42,7 @@ pub async fn list(token: String, sessions: State<'_, Sessions>) -> Result) -
let fetch_box = spawn_blocking(move || {
let mut session_lock = session.lock().unwrap();
- let mailbox = session_lock
- .get(&box_id)
- .map(|mailbox| mailbox.clone())?;
+ let mailbox = session_lock.get(&box_id).map(|mailbox| mailbox.clone())?;
Ok(mailbox)
});
@@ -81,8 +80,7 @@ pub async fn messages(
let fetch_message_list = spawn_blocking(move || {
let mut session_lock = session.lock().unwrap();
- let message_list = session_lock
- .messages(&box_id, start, end)?;
+ let message_list = session_lock.messages(&box_id, start, end)?;
Ok(message_list)
});
@@ -103,8 +101,7 @@ pub async fn get_message(
let fetch_message = spawn_blocking(move || {
let mut session_lock = session.lock().unwrap();
- let message = session_lock
- .get_message(&box_id, &message_id)?;
+ let message = session_lock.get_message(&box_id, &message_id)?;
Ok(message)
});
diff --git a/apps/web/src-tauri/src/types/session/utils.rs b/apps/web/src-tauri/src/types/session/utils.rs
index 78471b5..b120084 100644
--- a/apps/web/src-tauri/src/types/session/utils.rs
+++ b/apps/web/src-tauri/src/types/session/utils.rs
@@ -1,9 +1,10 @@
-use sdk::session::Credentials;
+use sdk::session::FullLoginOptions;
use crate::{
base64, cryptography,
files::CacheFile,
- types::{Error, ErrorKind, Result}, parse,
+ parse,
+ types::{Error, ErrorKind, Result},
};
/// Given a token, get the encryption nonce and key.
@@ -34,7 +35,7 @@ pub fn get_nonce_and_key_from_token(token: &str) -> Result<(String, String)> {
}
/// Given a token, find the corresponding encrypted login credentials in the cache dir and return them.
-pub fn get_credentials(token: &str) -> Result {
+pub fn get_credentials(token: &str) -> Result {
let (key_base64, nonce_base64) = get_nonce_and_key_from_token(token)?;
let key = base64::decode(key_base64.as_bytes())?;
@@ -56,7 +57,7 @@ pub fn get_credentials(token: &str) -> Result {
let decrypted = cryptography::decrypt(&encrypted, &key, &nonce)?;
- let login_options: Credentials = serde_json::from_slice(&decrypted).map_err(|e| {
+ let login_options: FullLoginOptions = serde_json::from_slice(&decrypted).map_err(|e| {
Error::new(
ErrorKind::DeserializeJSON,
format!("Could not deserialize encrypted login info {}", e),
@@ -66,7 +67,7 @@ pub fn get_credentials(token: &str) -> Result {
Ok(login_options)
}
-pub fn generate_token(credentials: &Credentials) -> Result {
+pub fn generate_token(credentials: &FullLoginOptions) -> Result {
// Serialize the given options to json
let options_json = parse::to_json(credentials)?;
diff --git a/apps/web/src/components/Login/Form.tsx b/apps/web/src/components/Login/Form.tsx
index c8f6404..ee03d49 100644
--- a/apps/web/src/components/Login/Form.tsx
+++ b/apps/web/src/components/Login/Form.tsx
@@ -43,16 +43,16 @@ import Typography from "@mui/material/Typography";
import VisibilityIcon from "@mui/icons-material/Visibility";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
-import MultiServerLoginOptions from "@interfaces/login";
-
import modalStyles from "@styles/modal";
import scrollbarStyles from "@styles/scrollbar";
import { useMailLogin } from "@utils/hooks/useLogin";
import useMailClient from "@utils/hooks/useMailClient";
import useMultiServerLoginStore, {
- defaultConfigs
+ defaultConfigs,
+ MultiServerLoginOptions
} from "@utils/hooks/useMultiServerLoginStore";
+import useOAuth2Client from "@utils/hooks/useOAuth2Client";
import useStore from "@utils/hooks/useStore";
import useTheme from "@utils/hooks/useTheme";
import parseEmail from "@utils/parseEmail";
@@ -263,6 +263,32 @@ const UnMemoizedServerConfigColumn: FC<{
const ServerConfigColumn = memo(UnMemoizedServerConfigColumn);
+const createCredentials = (
+ incomingConfig: MultiServerLoginOptions,
+ incomingType: IncomingMailServerType
+): Credentials => {
+ const {
+ username: incomingUsername,
+ password: incomingPassword,
+ ...incoming
+ } = incomingConfig;
+
+ const options: Credentials = {
+ incoming: {
+ ...incoming,
+ loginType: {
+ passwordBased: {
+ password: incomingPassword,
+ username: incomingUsername
+ }
+ }
+ },
+ incomingType
+ };
+
+ return options;
+};
+
const LoginOptionsMenu: FC = () => {
const theme = useTheme();
@@ -287,10 +313,10 @@ const LoginOptionsMenu: FC = () => {
const provider = useMultiServerLoginStore((state) => state.provider);
- const incoming = useMultiServerLoginStore(
+ const incomingConfig = useMultiServerLoginStore(
(state) => state.incoming[selectedMailServerTypes.incoming]
);
- const outgoing = useMultiServerLoginStore(
+ const outgoingConfig = useMultiServerLoginStore(
(state) => state.outgoing[selectedMailServerTypes.outgoing]
);
@@ -304,8 +330,8 @@ const LoginOptionsMenu: FC = () => {
}, [resetToDefaults, setOpen]);
const missingFields = useMemo(() => {
- return !incoming.username || !incoming.password;
- }, [incoming.username, incoming.password]);
+ return !incomingConfig.username || !incomingConfig.password;
+ }, [incomingConfig.username, incomingConfig.password]);
const onSubmit: FormEventHandler = async (e): Promise => {
e.preventDefault();
@@ -316,12 +342,12 @@ const LoginOptionsMenu: FC = () => {
return;
}
- const options: Credentials = {
- incoming,
- incoming_type: selectedMailServerTypes.incoming
- };
+ const credentials = createCredentials(
+ incomingConfig,
+ selectedMailServerTypes.incoming
+ );
- await login(options)
+ await login(credentials)
.then((result) => {
if (result.ok) {
onClose();
@@ -362,21 +388,21 @@ const LoginOptionsMenu: FC = () => {
@@ -433,6 +459,7 @@ const LoginForm: FC<{
const [error, setError] = useState();
const mailClient = useMailClient();
+ const oauthClient = useOAuth2Client();
useEffect(() => setError(undefined), [username, password]);
@@ -509,6 +536,13 @@ const LoginForm: FC<{
const config = configResult.data;
+ oauthClient.getGrant(
+ config.displayName,
+ config.oauth2?.oauthUrl ?? "",
+ config.oauth2?.tokenUrl ?? "",
+ config.oauth2?.scopes ?? []
+ );
+
if (
typeof config.type != "string" &&
config.type.multiServer?.incoming &&
@@ -517,7 +551,6 @@ const LoginForm: FC<{
const incomingConfigs: (MultiServerLoginOptions & {
type: IncomingMailServerType;
})[] = config.type.multiServer.incoming.map(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
({ authType, ...config }) => ({
...config,
loginType: authType,
@@ -529,7 +562,6 @@ const LoginForm: FC<{
const outgoingConfigs: (MultiServerLoginOptions & {
type: OutgoingMailServerType;
})[] = config.type.multiServer.outgoing.map(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
({ authType, ...config }) => ({
...config,
loginType: authType,
diff --git a/apps/web/src/interfaces/login.ts b/apps/web/src/interfaces/login.ts
deleted file mode 100644
index d5092a0..0000000
--- a/apps/web/src/interfaces/login.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { AuthType, ConnectionSecurity } from "@dust-mail/structures";
-
-export default interface MultiServerLoginOptions {
- username: string;
- password: string;
- domain: string;
- port: number;
- security: ConnectionSecurity;
- loginType: AuthType[];
-}
diff --git a/apps/web/src/interfaces/oauth2.ts b/apps/web/src/interfaces/oauth2.ts
new file mode 100644
index 0000000..a2b058d
--- /dev/null
+++ b/apps/web/src/interfaces/oauth2.ts
@@ -0,0 +1,10 @@
+import { Result } from "./result";
+
+export default interface OAuth2Client {
+ getGrant: (
+ providerName: string,
+ grantUrl: string,
+ tokenUrl: string,
+ scopes: string[]
+ ) => Promise>;
+}
diff --git a/apps/web/src/utils/avatarUrl.ts b/apps/web/src/utils/avatarUrl.ts
index e5f84c3..c2bdc34 100644
--- a/apps/web/src/utils/avatarUrl.ts
+++ b/apps/web/src/utils/avatarUrl.ts
@@ -3,14 +3,12 @@ import createMd5Hash from "js-md5";
const createAvatarUrl = (email: string): string => {
const hashed = createMd5Hash(email.trim().toLowerCase());
- const searchParams = new URLSearchParams();
+ const url = new URL("https://www.gravatar.com/avatar/" + hashed);
- searchParams.set("s", "64");
- searchParams.set("d", "404");
+ url.searchParams.set("s", "64");
+ url.searchParams.set("d", "404");
- return (
- "https://www.gravatar.com/avatar/" + hashed + "?" + searchParams.toString()
- );
+ return url.toString();
};
export default createAvatarUrl;
diff --git a/apps/web/src/utils/defaultErrors.ts b/apps/web/src/utils/defaultErrors.ts
new file mode 100644
index 0000000..cc20560
--- /dev/null
+++ b/apps/web/src/utils/defaultErrors.ts
@@ -0,0 +1,23 @@
+import { createBaseError } from "./parseError";
+
+import { ErrorResult } from "@interfaces/result";
+
+export const NotLoggedIn = (): ErrorResult =>
+ createBaseError({
+ kind: "NotLoggedIn",
+ message: "Could not find session token in local storage"
+ });
+
+export const NotImplemented = (feature?: string): ErrorResult =>
+ createBaseError({
+ kind: "NotImplemented",
+ message: `The feature ${
+ feature ? `'${feature}'` : ""
+ } is not yet implemented`
+ });
+
+export const MissingRequiredParam = (): ErrorResult =>
+ createBaseError({
+ kind: "MissingRequiredParam",
+ message: "Missing a required parameter"
+ });
diff --git a/apps/web/src/utils/hooks/useLogin.ts b/apps/web/src/utils/hooks/useLogin.ts
index f350322..1fa37fe 100644
--- a/apps/web/src/utils/hooks/useLogin.ts
+++ b/apps/web/src/utils/hooks/useLogin.ts
@@ -3,7 +3,12 @@ import useUser, { useCurrentUser, useModifyUser } from "./useUser";
import { useNavigate } from "react-router-dom";
-import { Credentials, ServerType, Version } from "@dust-mail/structures";
+import {
+ Credentials,
+ LoginOptions,
+ ServerType,
+ Version
+} from "@dust-mail/structures";
import { Result } from "@interfaces/result";
@@ -16,6 +21,29 @@ import {
// TODO: Fix this mess of a file.
+const findUsernameInLoginOptions = (loginOptions: LoginOptions): string => {
+ return (
+ loginOptions.loginType.oAuthBased?.username ??
+ loginOptions.loginType.passwordBased?.username ??
+ // Should never occur, but just in case it does happen there will be a fallback
+ "test@example.com"
+ );
+};
+
+const createIdentifier = (
+ incomingConfig: LoginOptions,
+ outgoingConfig: LoginOptions
+): string => {
+ const incomingUsername = findUsernameInLoginOptions(incomingConfig);
+ const outgoingUsername = findUsernameInLoginOptions(outgoingConfig);
+
+ const identifier = btoa(
+ `${incomingUsername}@${incomingConfig.domain}|${outgoingUsername}@${outgoingConfig.domain}`
+ );
+
+ return identifier;
+};
+
export const useMailLogin = (): ((
config: Credentials
) => Promise>) => {
@@ -80,15 +108,13 @@ export const useMailLogin = (): ((
message: "Config is missing items"
});
- const id = btoa(
- `${incomingConfig.username}@${incomingConfig.domain}|${outgoingConfig.username}@${outgoingConfig.domain}`
- );
+ const id = createIdentifier(incomingConfig, outgoingConfig);
login(loginResult.data, {
id,
usernames: {
- incoming: incomingConfig.username,
- outgoing: outgoingConfig.username
+ incoming: findUsernameInLoginOptions(incomingConfig),
+ outgoing: findUsernameInLoginOptions(outgoingConfig)
},
redirectToDashboard: true,
setAsDefault: true
diff --git a/apps/web/src/utils/hooks/useMailClient.ts b/apps/web/src/utils/hooks/useMailClient.ts
index c355145..cd040fe 100644
--- a/apps/web/src/utils/hooks/useMailClient.ts
+++ b/apps/web/src/utils/hooks/useMailClient.ts
@@ -17,8 +17,8 @@ import {
import { messageCountForPage } from "@src/constants";
import MailClient from "@interfaces/client";
-import { ErrorResult } from "@interfaces/result";
+import { MissingRequiredParam, NotLoggedIn } from "@utils/defaultErrors";
import parseEmail from "@utils/parseEmail";
import {
createBaseError,
@@ -27,26 +27,6 @@ import {
} from "@utils/parseError";
import parseZodOutput from "@utils/parseZodOutput";
-const NotLoggedIn = (): ErrorResult =>
- createBaseError({
- kind: "NotLoggedIn",
- message: "Could not find session token in local storage"
- });
-
-// const NotImplemented = (feature?: string): Error =>
-// createBaseError({
-// kind: "NotImplemented",
-// message: `The feature ${
-// feature ? `'${feature}'` : ""
-// } is not yet implemented`
-// });
-
-const MissingRequiredParam = (): ErrorResult =>
- createBaseError({
- kind: "MissingRequiredParam",
- message: "Missing a required parameter"
- });
-
const useMailClient = (): MailClient => {
const isTauri: boolean = "__TAURI__" in window;
diff --git a/apps/web/src/utils/hooks/useMultiServerLoginStore.ts b/apps/web/src/utils/hooks/useMultiServerLoginStore.ts
index 8fbaaea..ee1da51 100644
--- a/apps/web/src/utils/hooks/useMultiServerLoginStore.ts
+++ b/apps/web/src/utils/hooks/useMultiServerLoginStore.ts
@@ -10,10 +10,13 @@ import {
ServerType,
MailServerType,
ConnectionSecurity,
- AuthType
+ LoginOptions
} from "@dust-mail/structures";
-import MultiServerLoginOptions from "@interfaces/login";
+export type MultiServerLoginOptions = {
+ password: string;
+ username: string;
+} & Omit;
type Store = Record<
IncomingServerType,
@@ -49,57 +52,51 @@ type Store = Record<
) => (newValue?: string | number) => void;
};
-export const defaultIncomingServer: IncomingMailServerType = "Imap";
-export const defaultOutgoingServer: OutgoingMailServerType = "Smtp";
+export const defaultIncomingServer: IncomingMailServerType = "Imap" as const;
+export const defaultOutgoingServer: OutgoingMailServerType = "Smtp" as const;
-export const defaultUsername = "";
-export const defaultPassword = "";
+export const defaultUsername = "" as const;
+export const defaultPassword = "" as const;
export const defaultPorts: Record = {
Imap: 993,
Pop: 995,
Exchange: 443,
Smtp: 465
-};
+} as const;
export const defaultSecuritySetting: ConnectionSecurity = "Tls";
-const defaultLoginTypes: AuthType[] = ["ClearText"];
-
const defaultImapConfig: MultiServerLoginOptions = {
- password: defaultPassword,
username: defaultUsername,
+ password: defaultPassword,
domain: "imap.example.com",
security: defaultSecuritySetting,
- port: defaultPorts["Imap"],
- loginType: defaultLoginTypes
+ port: defaultPorts["Imap"]
};
const defaultExchangeConfig: MultiServerLoginOptions = {
- password: defaultPassword,
username: defaultUsername,
+ password: defaultPassword,
domain: "exchange.example.com",
security: defaultSecuritySetting,
- port: defaultPorts["Exchange"],
- loginType: defaultLoginTypes
+ port: defaultPorts["Exchange"]
};
const defaultPopConfig: MultiServerLoginOptions = {
- password: defaultPassword,
username: defaultUsername,
+ password: defaultPassword,
domain: "pop.example.com",
security: defaultSecuritySetting,
- port: defaultPorts["Pop"],
- loginType: defaultLoginTypes
+ port: defaultPorts["Pop"]
};
const defaultSmtpConfig: MultiServerLoginOptions = {
- password: defaultPassword,
username: defaultUsername,
+ password: defaultPassword,
domain: "smtp.example.com",
security: defaultSecuritySetting,
- port: defaultPorts["Smtp"],
- loginType: defaultLoginTypes
+ port: defaultPorts["Smtp"]
};
export const defaultConfigs = {
diff --git a/apps/web/src/utils/hooks/useOAuth2Client.ts b/apps/web/src/utils/hooks/useOAuth2Client.ts
new file mode 100644
index 0000000..74e7048
--- /dev/null
+++ b/apps/web/src/utils/hooks/useOAuth2Client.ts
@@ -0,0 +1,143 @@
+import z from "zod";
+
+import useFetchClient from "./useFetchClient";
+import useSettings from "./useSettings";
+
+import { OAuthState } from "@dust-mail/structures";
+
+import OAuth2Client from "@interfaces/oauth2";
+import { Result } from "@interfaces/result";
+
+import { NotImplemented } from "@utils/defaultErrors";
+import { createBaseError, createResultFromUnknown } from "@utils/parseError";
+import parseZodOutput from "@utils/parseZodOutput";
+
+const useGetPublicOAuthTokens = (): (() => Promise<
+ Result>
+>) => {
+ const fetch = useFetchClient();
+
+ return () =>
+ fetch("/mail/oauth2/tokens")
+ .then((response) => {
+ if (!response.ok) {
+ return response;
+ }
+
+ const output = z
+ .record(z.string(), z.string())
+ .safeParse(response.data);
+
+ return parseZodOutput(output);
+ })
+ .catch(createResultFromUnknown);
+};
+
+const findProviderToken = (
+ providerName: string,
+ tokens: Record
+): [string, string] | null => {
+ for (const [key, value] of Object.entries(tokens)) {
+ const isProvider = providerName
+ .toLowerCase()
+ .includes(key.trim().toLowerCase());
+
+ if (isProvider) return [value, key];
+ }
+
+ return null;
+};
+
+const useOAuth2Client = (): OAuth2Client => {
+ const isTauri: boolean = "__TAURI__" in window;
+
+ const getPublicTokens = useGetPublicOAuthTokens();
+
+ const [settings] = useSettings();
+
+ return {
+ async getGrant(providerName, authUrl, tokenUrl, scopes) {
+ const authUrlResult = z.string().url().safeParse(authUrl);
+
+ const authUrlOutput = parseZodOutput(authUrlResult);
+
+ if (!authUrlOutput.ok) {
+ return authUrlOutput;
+ }
+
+ if (settings.httpServerUrl === null)
+ return createBaseError({
+ kind: "NoBackend",
+ message: "Backend server url is not set"
+ });
+
+ const publicTokensResult = await getPublicTokens().catch(
+ createResultFromUnknown
+ );
+
+ if (!publicTokensResult.ok) {
+ return publicTokensResult;
+ }
+
+ const publicTokens = publicTokensResult.data;
+
+ const providerDetails = findProviderToken(providerName, publicTokens);
+
+ if (providerDetails === null)
+ return createBaseError({
+ kind: "NoOAuthToken",
+ message:
+ "Could not find a oauth token on remote Dust-Mail server to authorize with email provider"
+ });
+
+ const providerToken = providerDetails[0];
+ const providerId = providerDetails[1];
+
+ if (!isTauri) {
+ if (typeof window !== "undefined" && "open" in window) {
+ const url = new URL(authUrlOutput.data);
+ const redirectUri = new URL(
+ "/mail/oauth2/redirect",
+ settings.httpServerUrl
+ );
+
+ const state: OAuthState = {
+ provider: providerId,
+ application: isTauri ? "desktop" : "web"
+ };
+
+ // https://www.rfc-editor.org/rfc/rfc6749#section-1.1
+ url.searchParams.set("response_type", "code");
+ url.searchParams.set("redirect_uri", redirectUri.toString());
+ url.searchParams.set("client_id", providerToken);
+ url.searchParams.set("scope", scopes.join(" "));
+ url.searchParams.set("state", JSON.stringify(state));
+ url.searchParams.set("access_type", "offline");
+
+ const oauthLoginWindow = window.open(url, "_blank", "popup");
+
+ if (oauthLoginWindow === null)
+ return createBaseError({
+ kind: "UnsupportedEnvironment",
+ message:
+ "Your browser environment does not support intercommunication between windows"
+ });
+
+ oauthLoginWindow.addEventListener("message", console.log);
+
+ return { ok: true as const, data: "" };
+ } else {
+ return createBaseError({
+ kind: "UnsupportedEnvironment",
+ message:
+ "Your browser environment does not support opening a new window"
+ });
+ }
+ }
+
+ return NotImplemented("oauth-grant-tauri");
+ }
+ };
+};
+
+export default useOAuth2Client;
diff --git a/packages/pop3/src/lib.rs b/packages/pop3/src/lib.rs
index b9b103d..dfc31e1 100644
--- a/packages/pop3/src/lib.rs
+++ b/packages/pop3/src/lib.rs
@@ -407,6 +407,24 @@ impl Client {
Ok(())
}
+ pub fn auth>(&mut self, token: U) -> types::Result<()> {
+ self.is_correct_state(ClientState::Authentication)?;
+
+ self.has_capability_else_err(vec![Capability::Sasl(vec![String::from("XOAUTH2")])])?;
+
+ self.has_read_greeting()?;
+
+ let socket = self.get_socket_mut()?;
+
+ let command = format!("AUTH {}", token.as_ref());
+
+ socket.send_command(command.as_bytes(), false)?;
+
+ self.state = ClientState::Transaction;
+
+ Ok(())
+ }
+
pub fn login(&mut self, user: &str, password: &str) -> types::Result<()> {
self.is_correct_state(ClientState::Authentication)?;
diff --git a/packages/sdk/src/client/incoming.rs b/packages/sdk/src/client/incoming.rs
index a240145..4a64820 100644
--- a/packages/sdk/src/client/incoming.rs
+++ b/packages/sdk/src/client/incoming.rs
@@ -12,7 +12,7 @@ use crate::imap::{self, ImapClient};
use crate::pop::{self, PopClient};
use crate::types::{
- self, ConnectOptions, Error, ErrorKind, IncomingClientType, MailBox, Message, Preview,
+ Error, ErrorKind, IncomingClientType, MailBox, Message, OAuthCredentials, Preview, Result,
};
enum IncomingClientTypeWithClient
@@ -25,12 +25,17 @@ where
Pop(PopClient),
}
-pub struct IncomingClient {
+pub struct IncomingClient {
client: IncomingClientTypeWithClient,
}
impl IncomingClient {
- pub fn login(self, username: &str, password: &str) -> types::Result> {
+ /// Login to the specified mail server using a username and a password.
+ pub fn login>(
+ self,
+ username: T,
+ password: T,
+ ) -> Result> {
match self.client {
#[cfg(feature = "imap")]
IncomingClientTypeWithClient::Imap(client) => {
@@ -50,94 +55,141 @@ impl IncomingClient {
)),
}
}
+
+ /// Login to the specified mail servers using OAuth2 credentials, which consist of an access token and a usernames
+ pub fn oauth2_login(
+ self,
+ oauth_credentials: &OAuthCredentials,
+ ) -> Result> {
+ match self.client {
+ #[cfg(feature = "imap")]
+ IncomingClientTypeWithClient::Imap(client) => {
+ let session = client.oauth2_login(oauth_credentials)?;
+
+ Ok(Box::new(session))
+ }
+ // #[cfg(feature = "pop")]
+ // IncomingClientTypeWithClient::Pop(client) => {
+ // let session = client.login(username, password)?;
+
+ // Ok(Box::new(session))
+ // }
+ _ => Err(Error::new(
+ ErrorKind::NoClientAvailable,
+ "OAuth2 login is not supported by the specified incoming client type",
+ )),
+ }
+ }
}
-pub trait Session {
+pub trait IncomingSession {
/// Logout of the session, closing the connection with the server if applicable.
- fn logout(&mut self) -> types::Result<()>;
+ fn logout(&mut self) -> Result<()>;
/// Returns a list of all of the mailboxes that are on the server.
- fn box_list(&mut self) -> types::Result<&Vec>;
+ fn box_list(&mut self) -> Result<&Vec>;
/// Returns some basic information about a specified mailbox.
- fn get(&mut self, box_id: &str) -> types::Result<&MailBox>;
+ fn get(&mut self, box_id: &str) -> Result<&MailBox>;
/// Deletes a specified mailbox.
- fn delete(&mut self, box_id: &str) -> types::Result<()>;
+ fn delete(&mut self, box_id: &str) -> Result<()>;
/// Creates a new mailbox with a specified id.
- fn create(&mut self, box_id: &str) -> types::Result<()>;
+ fn create(&mut self, box_id: &str) -> Result<()>;
/// Renames a specified mailbox.
- fn rename(&mut self, box_id: &str, new_name: &str) -> types::Result<()>;
+ fn rename(&mut self, box_id: &str, new_name: &str) -> Result<()>;
/// Returns a list of a specified range of messages from a specified mailbox.
- fn messages(&mut self, box_id: &str, start: u32, end: u32) -> types::Result>;
+ fn messages(&mut self, box_id: &str, start: u32, end: u32) -> Result>;
/// Returns all of the relevant data for a specified message.
- fn get_message(&mut self, box_id: &str, msg_id: &str) -> types::Result;
+ fn get_message(&mut self, box_id: &str, msg_id: &str) -> Result;
+}
+
+/// A struct used to create a connection to an incoming mail server.
+///
+/// Use the `new()` method to build a connection.
+pub struct IncomingClientBuilder {
+ client_type: IncomingClientType,
+ server: Option,
+ port: Option,
}
-pub struct ClientConstructor;
-
-/// Check whether the login options exist if they are needed for a given client type.
-fn check_options(
- client_type: &IncomingClientType,
- options: &Option,
-) -> types::Result<()> {
- match client_type {
- #[cfg(feature = "imap")]
- IncomingClientType::Imap => match options {
- Some(_) => Ok(()),
+impl IncomingClientBuilder {
+ /// Creates an incoming client builder given an incoming client type.
+ ///
+ /// This incoming client type will specify what kind of protocol is going to be used to connect to the later specified mail server.
+ pub fn new(client_type: &IncomingClientType) -> Self {
+ Self {
+ client_type: client_type.clone(),
+ port: None,
+ server: None,
+ }
+ }
+
+ /// Set the port of the server to connect to.
+ ///
+ /// E.g `993`
+ pub fn set_port>(&mut self, port: S) -> &mut Self {
+ self.port = Some(port.into());
+
+ self
+ }
+
+ /// Set the domain name of the server to connect to.
+ ///
+ /// E.g `example.com`
+ pub fn set_server>(&mut self, server: S) -> &mut Self {
+ self.server = Some(server.into());
+
+ self
+ }
+
+ /// Internal function used to check if the domain and port have been set and return them if they are set.
+ ///
+ /// This function will error if either one is not set.
+ fn get_connect_config(&self) -> Result<(&str, &u16)> {
+ let port = match self.port.as_ref() {
+ Some(port) => port,
None => {
- return Err(types::Error::new(
- types::ErrorKind::Unsupported,
- "Imap support requires the login options to be specified",
+ return Err(Error::new(
+ ErrorKind::InvalidLoginConfig,
+ "Missing port from login config",
))
}
- },
- #[cfg(feature = "pop")]
- IncomingClientType::Pop => match options {
- Some(_) => Ok(()),
+ };
+
+ let server = match self.server.as_ref() {
+ Some(server) => server,
None => {
- return Err(types::Error::new(
- types::ErrorKind::Unsupported,
- "Pop support requires the login options to be specified",
+ return Err(Error::new(
+ ErrorKind::InvalidLoginConfig,
+ "Missing server from login config",
))
}
- },
+ };
+
+ return Ok((server, port));
}
-}
-impl ClientConstructor {
/// Creates a new client over a secure tcp connection.
- pub fn new(
- client_type: &IncomingClientType,
- options: Option,
- ) -> types::Result>> {
- check_options(&client_type, &options)?;
-
- let client = match client_type {
+ pub fn build(&self) -> Result>> {
+ let client = match self.client_type {
#[cfg(feature = "imap")]
IncomingClientType::Imap => {
- let options = match options {
- Some(options) => options,
- // Is unreachable as we have already checked the options for every client type
- None => unreachable!(),
- };
+ let (server, port) = self.get_connect_config()?;
- let client = imap::connect(options)?;
+ let client = imap::connect(server, port.clone())?;
IncomingClientTypeWithClient::Imap(client)
}
#[cfg(feature = "pop")]
IncomingClientType::Pop => {
- let options = match options {
- Some(options) => options,
- None => unreachable!(),
- };
+ let (server, port) = self.get_connect_config()?;
- let client = pop::connect(options)?;
+ let client = pop::connect(server, port.clone())?;
IncomingClientTypeWithClient::Pop(client)
}
@@ -148,33 +200,22 @@ impl ClientConstructor {
/// Creates a new client over a plain tcp connection.
///
- /// # Do not use this in a production environment as it will send your credentials to the server without any encryption!
- pub fn new_plain(
- client_type: &IncomingClientType,
- options: Option,
- ) -> types::Result> {
- check_options(&client_type, &options)?;
-
- let client = match client_type {
+ /// ### Do not use this in a production environment as it will send your credentials to the server without any encryption!
+ pub fn build_plain(&self) -> Result> {
+ let client = match self.client_type {
#[cfg(feature = "imap")]
IncomingClientType::Imap => {
- let options = match options {
- Some(options) => options,
- None => unreachable!(),
- };
+ let (server, port) = self.get_connect_config()?;
- let client = imap::connect_plain(options)?;
+ let client = imap::connect_plain(server, port.clone())?;
IncomingClientTypeWithClient::Imap(client)
}
#[cfg(feature = "pop")]
IncomingClientType::Pop => {
- let options = match options {
- Some(options) => options,
- None => unreachable!(),
- };
+ let (server, port) = self.get_connect_config()?;
- let client = pop::connect_plain(options)?;
+ let client = pop::connect_plain(server, port.clone())?;
IncomingClientTypeWithClient::Pop(client)
}
@@ -183,51 +224,3 @@ impl ClientConstructor {
Ok(IncomingClient { client })
}
}
-
-#[cfg(test)]
-mod tests {
- use crate::types::ConnectOptions;
-
- use std::env;
-
- use dotenv::dotenv;
-
- use super::{IncomingClientType, Session};
-
- fn create_session() -> Box {
- dotenv().ok();
-
- let username = env::var("IMAP_USERNAME").unwrap();
- let password = env::var("IMAP_PASSWORD").unwrap();
-
- let server = env::var("IMAP_SERVER").unwrap();
- let port: u16 = 993;
-
- let options = ConnectOptions::new(server, &port);
-
- let client =
- super::ClientConstructor::new(&IncomingClientType::Imap, Some(options)).unwrap();
-
- let session = client.login(&username, &password).unwrap();
-
- session
- }
-
- #[test]
- fn logout() {
- let mut session = create_session();
-
- session.logout().unwrap();
- }
-
- #[test]
- fn box_list() {
- let mut session = create_session();
-
- let list = session.box_list().unwrap();
-
- for mailbox in list {
- println!("{}", mailbox.counts().unwrap().total());
- }
- }
-}
diff --git a/packages/sdk/src/client/mod.rs b/packages/sdk/src/client/mod.rs
index 28f2603..f8327bd 100644
--- a/packages/sdk/src/client/mod.rs
+++ b/packages/sdk/src/client/mod.rs
@@ -1 +1,4 @@
pub mod incoming;
+
+#[cfg(test)]
+mod test;
diff --git a/packages/sdk/src/client/test.rs b/packages/sdk/src/client/test.rs
new file mode 100644
index 0000000..8c60026
--- /dev/null
+++ b/packages/sdk/src/client/test.rs
@@ -0,0 +1,45 @@
+use std::env;
+
+use dotenv::dotenv;
+
+use crate::types::IncomingClientType;
+
+use super::incoming::{IncomingClientBuilder, IncomingSession};
+
+fn create_session() -> Box {
+ dotenv().ok();
+
+ let username = env::var("IMAP_USERNAME").unwrap();
+ let password = env::var("IMAP_PASSWORD").unwrap();
+
+ let server = env::var("IMAP_SERVER").unwrap();
+ let port: u16 = 993;
+
+ let client = IncomingClientBuilder::new(&IncomingClientType::Imap)
+ .set_server(server)
+ .set_port(port)
+ .build()
+ .unwrap();
+
+ let session = client.login(&username, &password).unwrap();
+
+ session
+}
+
+#[test]
+fn logout() {
+ let mut session = create_session();
+
+ session.logout().unwrap();
+}
+
+#[test]
+fn box_list() {
+ let mut session = create_session();
+
+ let list = session.box_list().unwrap();
+
+ for mailbox in list {
+ println!("{}", mailbox.counts().unwrap().total());
+ }
+}
diff --git a/packages/sdk/src/detect/mod.rs b/packages/sdk/src/detect/mod.rs
index e732b06..f548a2c 100644
--- a/packages/sdk/src/detect/mod.rs
+++ b/packages/sdk/src/detect/mod.rs
@@ -220,6 +220,7 @@ pub async fn from_email(email_address: &str) -> Result {
config = Some(Config::new(
ConfigType::new_multiserver(incoming_configs, outgoing_configs),
domain.as_str(),
+ None,
Some(domain.to_string()),
));
}
diff --git a/packages/sdk/src/detect/parse.rs b/packages/sdk/src/detect/parse.rs
index 90f2fb1..0f31f6d 100644
--- a/packages/sdk/src/detect/parse.rs
+++ b/packages/sdk/src/detect/parse.rs
@@ -3,13 +3,16 @@ use autoconfig::{
self,
types::config::{
AuthenticationType as AutoConfigAuthenticationType, Config as AutoConfig,
- SecurityType as AutoConfigSecurityType, Server, ServerType as AutoConfigServerType,
+ OAuth2Config as AutoConfigOAuth2Config, SecurityType as AutoConfigSecurityType, Server,
+ ServerType as AutoConfigServerType,
},
};
use crate::types::{ConnectionSecurity, Result};
-use super::{AuthenticationType, Config, ConfigType, ServerConfig, ServerConfigType};
+use super::{
+ types::OAuth2Config, AuthenticationType, Config, ConfigType, ServerConfig, ServerConfigType,
+};
#[cfg(feature = "autoconfig")]
pub struct AutoConfigParser;
@@ -20,7 +23,7 @@ impl AutoConfigParser {
{
let domain: String = server.hostname()?.into();
- let port: u16 = *server.port()?;
+ let port: u16 = server.port().cloned()?;
let security: ConnectionSecurity = match server.security_type() {
Some(security) => match security {
@@ -60,8 +63,17 @@ impl AutoConfigParser {
}
}
+ fn parse_oauth2_config(config: &AutoConfigOAuth2Config) -> OAuth2Config {
+ OAuth2Config::new(
+ config.token_url().into(),
+ config.auth_url().into(),
+ config.scope(),
+ )
+ }
+
pub fn parse(autoconfig: AutoConfig) -> Result {
let provider: String = autoconfig.email_provider().id().into();
+
let display_name: Option = autoconfig
.email_provider()
.display_name()
@@ -83,7 +95,9 @@ impl AutoConfigParser {
let config_type = ConfigType::MultiServer { incoming, outgoing };
- let config = Config::new(config_type, provider, display_name);
+ let oauth2_config = autoconfig.oauth2().map(Self::parse_oauth2_config);
+
+ let config = Config::new(config_type, provider, oauth2_config, display_name);
Ok(config)
}
diff --git a/packages/sdk/src/detect/types.rs b/packages/sdk/src/detect/types.rs
index 6b83ede..fe45be7 100644
--- a/packages/sdk/src/detect/types.rs
+++ b/packages/sdk/src/detect/types.rs
@@ -70,6 +70,37 @@ pub enum AuthenticationType {
Unknown,
}
+#[derive(Debug, Serialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct OAuth2Config {
+ token_url: String,
+ oauth_url: String,
+ scopes: Vec,
+}
+
+impl OAuth2Config {
+ pub fn new>(token_url: S, oauth_url: S, scopes: Vec) -> Self {
+ let scopes = scopes.into_iter().map(|scope| scope.into()).collect();
+
+ Self {
+ oauth_url: oauth_url.into(),
+ token_url: token_url.into(),
+ scopes,
+ }
+ }
+ pub fn oauth_url(&self) -> &str {
+ &self.oauth_url
+ }
+
+ pub fn token_url(&self) -> &str {
+ &self.token_url
+ }
+
+ pub fn scopes(&self) -> &Vec {
+ &self.scopes
+ }
+}
+
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub enum ConfigType {
@@ -90,6 +121,7 @@ impl ConfigType {
pub struct Config {
r#type: ConfigType,
provider: String,
+ oauth2: Option,
display_name: Option,
}
@@ -97,15 +129,21 @@ impl Config {
pub fn new>(
r#type: ConfigType,
provider: S,
+ oauth2_config: Option,
display_name: Option,
) -> Self {
Self {
display_name,
+ oauth2: oauth2_config,
provider: provider.into(),
r#type,
}
}
+ pub fn oauth2(&self) -> &Option {
+ &self.oauth2
+ }
+
/// The kind of config
pub fn config_type(&self) -> &ConfigType {
&self.r#type
diff --git a/packages/sdk/src/imap/mod.rs b/packages/sdk/src/imap/mod.rs
index 4c45bc3..7261c57 100644
--- a/packages/sdk/src/imap/mod.rs
+++ b/packages/sdk/src/imap/mod.rs
@@ -9,16 +9,16 @@ use std::time::{Duration, Instant};
use imap::types::{Fetch as ImapFetch, Mailbox as ImapMailbox};
use native_tls::TlsStream;
-use crate::client::incoming::Session;
+use crate::client::incoming::IncomingSession;
use crate::parse::map_imap_error;
use crate::tls::create_tls_connector;
use crate::types::{
- ConnectOptions,
// Counts,
Error,
ErrorKind,
MailBox,
Message,
+ OAuthCredentials,
Preview,
Result,
};
@@ -44,24 +44,25 @@ pub struct ImapSession {
selected_box: Option<(String, ImapMailbox)>,
}
-pub fn connect(options: ConnectOptions) -> Result>> {
+pub fn connect, P: Into>(
+ server: S,
+ port: P,
+) -> Result>> {
let tls = create_tls_connector()?;
- let domain = options.server();
- let port = *options.port();
-
- let client = imap::connect((domain, port), domain, &tls).map_err(map_imap_error)?;
+ let client = imap::connect((server.as_ref(), port.into()), server.as_ref(), &tls)
+ .map_err(map_imap_error)?;
let imap_client = ImapClient { client };
Ok(imap_client)
}
-pub fn connect_plain(options: ConnectOptions) -> Result> {
- let domain = options.server();
- let port = *options.port();
-
- let stream = TcpStream::connect((domain, port))
+pub fn connect_plain, P: Into>(
+ server: S,
+ port: P,
+) -> Result> {
+ let stream = TcpStream::connect((server.as_ref(), port.into()))
.map_err(|e| Error::new(ErrorKind::Connect, e.to_string()))?;
let client = imap::Client::new(stream);
@@ -70,7 +71,7 @@ pub fn connect_plain(options: ConnectOptions) -> Result> {
}
impl ImapClient {
- pub fn login(self, username: &str, password: &str) -> Result> {
+ pub fn login>(self, username: T, password: T) -> Result> {
let session = self
.client
.login(username, password)
@@ -80,7 +81,22 @@ impl ImapClient {
session,
box_list: Vec::new(),
box_list_last_refresh: None,
- // retrieved_message_counts: false,
+ selected_box: None,
+ };
+
+ Ok(imap_session)
+ }
+
+ pub fn oauth2_login(self, login: &OAuthCredentials) -> Result> {
+ let session = self
+ .client
+ .authenticate("XOAUTH2", login)
+ .map_err(|(err, _)| map_imap_error(err))?;
+
+ let imap_session = ImapSession {
+ session,
+ box_list: Vec::new(),
+ box_list_last_refresh: None,
selected_box: None,
};
@@ -165,7 +181,7 @@ impl ImapSession {
}
}
-impl Session for ImapSession {
+impl IncomingSession for ImapSession {
fn logout(&mut self) -> Result<()> {
let session = self.get_session_mut();
@@ -391,9 +407,9 @@ mod tests {
use native_tls::TlsStream;
- use super::{ConnectOptions, ImapSession};
+ use super::ImapSession;
- use crate::client::incoming::Session;
+ use crate::client::incoming::IncomingSession;
use dotenv::dotenv;
@@ -408,9 +424,7 @@ mod tests {
let server = env::var("IMAP_SERVER").unwrap();
let port: u16 = 993;
- let options = ConnectOptions::new(server, &port);
-
- let client = super::connect(options).unwrap();
+ let client = super::connect(server, port).unwrap();
let session = client.login(&username, &password).unwrap();
diff --git a/packages/sdk/src/lib.rs b/packages/sdk/src/lib.rs
index 5522eab..837e7ec 100644
--- a/packages/sdk/src/lib.rs
+++ b/packages/sdk/src/lib.rs
@@ -16,6 +16,4 @@ pub mod types;
pub mod session;
-pub use client::incoming::{
- ClientConstructor as IncomingClientConstructor, Session as IncomingSession,
-};
+pub use client::incoming::{IncomingClientBuilder, IncomingSession};
diff --git a/packages/sdk/src/pop/mod.rs b/packages/sdk/src/pop/mod.rs
index fc9e1c9..37a3476 100644
--- a/packages/sdk/src/pop/mod.rs
+++ b/packages/sdk/src/pop/mod.rs
@@ -6,10 +6,10 @@ use native_tls::TlsStream;
use pop3::types::{Left, Right};
use crate::{
- client::incoming::Session,
+ client::incoming::IncomingSession,
parse::{map_pop_error, parse_headers, parse_rfc822},
tls::create_tls_connector,
- types::{ConnectOptions, Counts, Error, ErrorKind, Flag, MailBox, Message, Preview, Result},
+ types::{Counts, Error, ErrorKind, Flag, MailBox, Message, Preview, Result},
};
use parse::parse_address;
@@ -28,31 +28,35 @@ pub struct PopSession {
unique_id_map: HashMap,
}
-pub fn connect(options: ConnectOptions) -> Result>> {
+pub fn connect, P: Into>(
+ server: S,
+ port: P,
+) -> Result>> {
let tls = create_tls_connector()?;
- let server = options.server();
- let port = *options.port();
-
- let session = pop3::connect((server, port), server, &tls, None).map_err(map_pop_error)?;
+ let session = pop3::connect((server.as_ref(), port.into()), server.as_ref(), &tls, None)
+ .map_err(map_pop_error)?;
Ok(PopClient { session })
}
-pub fn connect_plain(options: ConnectOptions) -> Result> {
- let server = options.server();
- let port = *options.port();
-
- let session = pop3::connect_plain((server, port), None).map_err(map_pop_error)?;
+pub fn connect_plain, P: Into>(
+ server: S,
+ port: P,
+) -> Result> {
+ let session =
+ pop3::connect_plain((server.as_ref(), port.into()), None).map_err(map_pop_error)?;
Ok(PopClient { session })
}
impl PopClient {
- pub fn login(self, username: &str, password: &str) -> Result> {
+ pub fn login>(self, username: T, password: T) -> Result> {
let mut session = self.session;
- session.login(username, password).map_err(map_pop_error)?;
+ session
+ .login(username.as_ref(), password.as_ref())
+ .map_err(map_pop_error)?;
// session.capabilities()
@@ -123,7 +127,7 @@ impl PopSession {
}
}
-impl Session for PopSession {
+impl IncomingSession for PopSession {
fn logout(&mut self) -> Result<()> {
let session = self.get_session_mut();
@@ -276,7 +280,7 @@ mod test {
use super::PopSession;
- use crate::{client::incoming::Session, types::ConnectOptions};
+ use crate::client::incoming::IncomingSession;
use dotenv::dotenv;
use native_tls::TlsStream;
@@ -291,9 +295,7 @@ mod test {
let server = env::var("POP_SERVER").unwrap();
let port: u16 = 995;
- let options = ConnectOptions::new(&server, &port);
-
- let client = super::connect(options).unwrap();
+ let client = super::connect(server, port).unwrap();
let session = client.login(&username, &password).unwrap();
diff --git a/packages/sdk/src/session/incoming.rs b/packages/sdk/src/session/incoming.rs
index 3d03f2f..071273d 100644
--- a/packages/sdk/src/session/incoming.rs
+++ b/packages/sdk/src/session/incoming.rs
@@ -1,26 +1,47 @@
+use std::io::{Read, Write};
+
use crate::{
- client::incoming::{ClientConstructor as IncomingClientConstructor, Session},
- types::{ConnectOptions, ConnectionSecurity, LoginOptions, Result, IncomingClientType},
+ client::incoming::{IncomingClient, IncomingClientBuilder, IncomingSession},
+ types::{ConnectionSecurity, IncomingClientType, Result},
};
-pub fn create_session(
+use super::login::{LoginOptions, LoginType};
+
+fn create_session_from_client(
+ client: IncomingClient,
+ login_type: &LoginType,
+) -> Result> {
+ match login_type {
+ LoginType::PasswordBased(password_creds) => {
+ client.login(password_creds.username(), password_creds.password())
+ }
+ LoginType::OAuthBased(oauth_creds) => client.oauth2_login(oauth_creds),
+ }
+}
+
+/// Given some login options and a client type, create an incoming session.
+///
+/// This will automatically connect and login to the mail server specified in the login options using the credentials specified in the login options.
+pub fn create_incoming_session(
options: &LoginOptions,
client_type: &IncomingClientType,
-) -> Result> {
- let client_options = ConnectOptions::new(options.domain(), options.port());
+) -> Result> {
+ let mut builder = IncomingClientBuilder::new(client_type);
+
+ builder
+ .set_server(options.domain())
+ .set_port(options.port().clone());
match options.security() {
ConnectionSecurity::Tls => {
- let client = IncomingClientConstructor::new(client_type, Some(client_options))?;
+ let client = builder.build()?;
- client
- .login(options.username(), options.password())
+ create_session_from_client(client, options.login_type())
}
ConnectionSecurity::Plain => {
- let client = IncomingClientConstructor::new_plain(client_type, Some(client_options))?;
+ let client = builder.build_plain()?;
- client
- .login(options.username(), options.password())
+ create_session_from_client(client, options.login_type())
}
_ => {
todo!()
diff --git a/packages/sdk/src/session/login.rs b/packages/sdk/src/session/login.rs
new file mode 100644
index 0000000..2812e22
--- /dev/null
+++ b/packages/sdk/src/session/login.rs
@@ -0,0 +1,197 @@
+use serde::{Deserialize, Serialize};
+
+use crate::types::{ConnectionSecurity, IncomingClientType, OAuthCredentials};
+
+#[derive(Deserialize, Serialize, Clone)]
+pub struct PasswordCredentials {
+ username: String,
+ password: String,
+}
+
+impl PasswordCredentials {
+ pub fn password(&self) -> &str {
+ &self.password
+ }
+
+ pub fn username(&self) -> &str {
+ &self.username
+ }
+
+ pub fn new>(username: S, password: S) -> Self {
+ Self {
+ password: password.into(),
+ username: username.into(),
+ }
+ }
+}
+
+#[derive(Deserialize, Serialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub enum LoginType {
+ PasswordBased(PasswordCredentials),
+ OAuthBased(OAuthCredentials),
+}
+
+#[derive(Deserialize, Serialize, Clone)]
+#[serde(rename_all = "camelCase")]
+/// A struct that specifies all of the need options and credentials to login to a mail server.
+pub struct LoginOptions {
+ login_type: LoginType,
+ domain: String,
+ port: u16,
+ security: ConnectionSecurity,
+}
+
+impl LoginOptions {
+ pub fn domain(&self) -> &str {
+ &self.domain
+ }
+
+ pub fn set_domain>(&mut self, domain: S) {
+ self.domain = domain.into();
+ }
+
+ pub fn port(&self) -> &u16 {
+ &self.port
+ }
+
+ pub fn set_port>(&mut self, port: u16) {
+ self.port = port.into();
+ }
+
+ pub fn security(&self) -> &ConnectionSecurity {
+ &self.security
+ }
+
+ pub fn login_type(&self) -> &LoginType {
+ &self.login_type
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+/// The bigger brother of `LoginOptions`.
+///
+/// Contains all of the information needed to login to an incoming and outgoing mail server and specifies what protocol is need to do so.
+pub struct FullLoginOptions {
+ incoming: LoginOptions,
+ incoming_type: IncomingClientType,
+}
+
+impl FullLoginOptions {
+ pub fn incoming_options(&self) -> &LoginOptions {
+ &self.incoming
+ }
+
+ pub fn incoming_type(&self) -> &IncomingClientType {
+ &self.incoming_type
+ }
+}
+
+pub struct FullLoginOptionsBuilder {
+ incoming_type: IncomingClientType,
+ domain: Option,
+ port: Option,
+ security: Option,
+ username: Option,
+ password: Option,
+ access_token: Option,
+}
+
+impl FullLoginOptionsBuilder {
+ pub fn new(incoming_type: &IncomingClientType) -> Self {
+ Self {
+ incoming_type: incoming_type.clone(),
+ access_token: None,
+ domain: None,
+ password: None,
+ port: None,
+ security: None,
+ username: None,
+ }
+ }
+
+ pub fn domain>(&mut self, domain: S) -> &mut Self {
+ self.domain = Some(domain.into());
+
+ self
+ }
+
+ pub fn port(&mut self, port: u16) -> &mut Self {
+ self.port = Some(port);
+
+ self
+ }
+
+ pub fn security(&mut self, security: ConnectionSecurity) -> &mut Self {
+ self.security = Some(security);
+
+ self
+ }
+
+ pub fn username>(&mut self, username: S) -> &mut Self {
+ self.username = Some(username.into());
+
+ self
+ }
+
+ pub fn password>(&mut self, password: S) -> &mut Self {
+ self.password = Some(password.into());
+
+ self
+ }
+
+ pub fn access_token>(&mut self, access_token: S) -> &mut Self {
+ self.access_token = Some(access_token.into());
+
+ self
+ }
+
+ pub fn build(self) -> Option {
+ let domain = self.domain?;
+ let port = self.port?;
+ let security = self.security?;
+ let username = self.username?;
+
+ let password = self.password;
+ let access_token = self.access_token;
+
+ let mut login_type: Option = None;
+
+ if password.is_some() {
+ let password_creds = PasswordCredentials::new(username, password.unwrap());
+
+ login_type = Some(LoginType::PasswordBased(password_creds));
+ } else if access_token.is_some() {
+ let oauth_creds = OAuthCredentials::new(username, access_token.unwrap());
+
+ login_type = Some(LoginType::OAuthBased(oauth_creds));
+ }
+
+ let login_options = LoginOptions {
+ login_type: login_type?,
+ domain,
+ port,
+ security,
+ };
+
+ Some(FullLoginOptions {
+ incoming: login_options,
+ incoming_type: self.incoming_type,
+ })
+ }
+}
+
+impl Into for FullLoginOptions {
+ /// Creates an identifiable string from the credentials, excluding the password.
+ fn into(self) -> String {
+ let incoming = self.incoming_options();
+
+ let username = match incoming.login_type() {
+ LoginType::OAuthBased(oauth_creds) => oauth_creds.username(),
+ LoginType::PasswordBased(password_creds) => password_creds.username(),
+ };
+
+ format!("{}@{}:{}", username, incoming.domain(), incoming.port())
+ }
+}
diff --git a/packages/sdk/src/session/mod.rs b/packages/sdk/src/session/mod.rs
index 3a4bdd8..3f9d456 100644
--- a/packages/sdk/src/session/mod.rs
+++ b/packages/sdk/src/session/mod.rs
@@ -1,14 +1,15 @@
mod incoming;
+mod login;
use std::sync::{Arc, Mutex};
-use serde::{Deserialize, Serialize};
use tokio::task::spawn_blocking;
-use crate::{
- types::{IncomingClientType, LoginOptions, Result},
- IncomingSession,
-};
+use crate::{types::Result, IncomingSession};
+
+use self::incoming::create_incoming_session;
+
+pub use self::login::{FullLoginOptions, FullLoginOptionsBuilder};
pub type ThreadSafeIncomingSession = Arc>>;
@@ -28,44 +29,7 @@ impl MailSessions {
}
}
-#[derive(Serialize, Deserialize, Clone)]
-pub struct Credentials {
- incoming: LoginOptions,
- incoming_type: IncomingClientType,
-}
-
-impl Credentials {
- pub fn incoming_options(&self) -> &LoginOptions {
- &self.incoming
- }
-
- pub fn incoming_type(&self) -> &IncomingClientType {
- &self.incoming_type
- }
-
- pub fn new(incoming: (LoginOptions, IncomingClientType)) -> Self {
- Self {
- incoming: incoming.0,
- incoming_type: incoming.1,
- }
- }
-}
-
-impl Into for Credentials {
- /// Creates an identifiable string from the credentials, excluding the password.
- fn into(self) -> String {
- let incoming = self.incoming_options();
-
- format!(
- "{}@{}:{}",
- incoming.username(),
- incoming.domain(),
- incoming.port()
- )
- }
-}
-
-pub async fn create_sessions(credentials: &Credentials) -> Result {
+pub async fn create_sessions(credentials: &FullLoginOptions) -> Result {
let incoming_credentials = (
credentials.incoming_options().clone(),
credentials.incoming_type().clone(),
@@ -73,7 +37,7 @@ pub async fn create_sessions(credentials: &Credentials) -> Result
// Try to get a session for all of the given login options
let incoming_login_thread = spawn_blocking(move || {
- incoming::create_session(&incoming_credentials.0, &incoming_credentials.1)
+ create_incoming_session(&incoming_credentials.0, &incoming_credentials.1)
});
// let outgoing_login_thread;
diff --git a/packages/sdk/src/types/connection.rs b/packages/sdk/src/types/connection.rs
new file mode 100644
index 0000000..51726f7
--- /dev/null
+++ b/packages/sdk/src/types/connection.rs
@@ -0,0 +1,8 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub enum ConnectionSecurity {
+ Tls,
+ StartTls,
+ Plain,
+}
diff --git a/packages/sdk/src/types/login.rs b/packages/sdk/src/types/login.rs
deleted file mode 100644
index 5f59960..0000000
--- a/packages/sdk/src/types/login.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use serde::{Deserialize, Serialize};
-
-pub struct ConnectOptions {
- server: String,
- port: u16,
-}
-
-impl ConnectOptions {
- pub fn new>(server: S, port: &u16) -> Self {
- Self {
- server: server.into(),
- port: *port,
- }
- }
-
- pub fn server(&self) -> &str {
- &self.server
- }
-
- pub fn port(&self) -> &u16 {
- &self.port
- }
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub enum ConnectionSecurity {
- Tls,
- StartTls,
- Plain,
-}
-
-#[derive(Deserialize, Serialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct LoginOptions {
- username: String,
- password: String,
- domain: String,
- port: u16,
- security: ConnectionSecurity,
-}
-
-impl LoginOptions {
- pub fn domain(&self) -> &str {
- &self.domain
- }
-
- pub fn port(&self) -> &u16 {
- &self.port
- }
-
- pub fn security(&self) -> &ConnectionSecurity {
- &self.security
- }
-
- pub fn username(&self) -> &str {
- &self.username
- }
-
- pub fn password(&self) -> &str {
- &self.password
- }
-}
diff --git a/packages/sdk/src/types/mod.rs b/packages/sdk/src/types/mod.rs
index 26dfcda..1c448d2 100644
--- a/packages/sdk/src/types/mod.rs
+++ b/packages/sdk/src/types/mod.rs
@@ -1,18 +1,20 @@
mod client;
+mod connection;
mod error;
mod flags;
-mod login;
mod mailbox;
mod message;
+mod oauth2;
use std::{collections::HashMap, result};
pub use client::*;
+pub use connection::ConnectionSecurity;
pub use error::{Error, ErrorKind};
pub use flags::Flag;
-pub use login::{ConnectOptions, ConnectionSecurity, LoginOptions};
pub use mailbox::{Counts, MailBox};
pub use message::{Address, Content, Message, Preview};
+pub use oauth2::OAuthCredentials;
pub type Result = result::Result;
diff --git a/packages/sdk/src/types/oauth2.rs b/packages/sdk/src/types/oauth2.rs
new file mode 100644
index 0000000..1e2f887
--- /dev/null
+++ b/packages/sdk/src/types/oauth2.rs
@@ -0,0 +1,37 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Deserialize, Serialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct OAuthCredentials {
+ username: String,
+ access_token: String,
+}
+
+impl OAuthCredentials {
+ pub fn new>(username: S, access_token: S) -> Self {
+ Self {
+ access_token: access_token.into(),
+ username: username.into(),
+ }
+ }
+
+ pub fn username(&self) -> &str {
+ &self.username
+ }
+
+ pub fn access_token(&self) -> &str {
+ &self.access_token
+ }
+}
+
+#[cfg(feature = "imap")]
+impl imap::Authenticator for OAuthCredentials {
+ type Response = String;
+
+ fn process(&self, _: &[u8]) -> Self::Response {
+ format!(
+ "user={}\x01auth=Bearer {}\x01\x01",
+ self.username, self.access_token
+ )
+ }
+}
diff --git a/packages/structures/package.json b/packages/structures/package.json
index 8b5cf64..ddf3720 100644
--- a/packages/structures/package.json
+++ b/packages/structures/package.json
@@ -18,6 +18,6 @@
"typescript": "^4.7.4"
},
"dependencies": {
- "zod": "^3.20.2"
+ "zod": "^3.21.4"
}
}
diff --git a/packages/structures/src/api/index.ts b/packages/structures/src/api/index.ts
index 53c9a27..e9ca7c8 100644
--- a/packages/structures/src/api/index.ts
+++ b/packages/structures/src/api/index.ts
@@ -22,3 +22,4 @@ export const ApiResponseModel = z.union([
export type ApiResponse = ApiOkResponse | AppError;
export * from "./settings";
+export * from "./oauth";
diff --git a/packages/structures/src/api/oauth.ts b/packages/structures/src/api/oauth.ts
new file mode 100644
index 0000000..9619cb3
--- /dev/null
+++ b/packages/structures/src/api/oauth.ts
@@ -0,0 +1,7 @@
+import z from "zod";
+
+export const OAuthStateModel = z.object({
+ provider: z.string(),
+ application: z.enum(["desktop", "web"])
+});
+export type OAuthState = z.infer;
diff --git a/packages/structures/src/api/settings.ts b/packages/structures/src/api/settings.ts
index 7a14bcc..38c367f 100644
--- a/packages/structures/src/api/settings.ts
+++ b/packages/structures/src/api/settings.ts
@@ -2,6 +2,6 @@ import z from "zod";
export const ApiSettingsModel = z.object({
authorization: z.boolean(),
- authorization_type: z.enum(["password", "user"])
+ authorization_type: z.enum(["password", "user"]).nullable()
});
export type ApiSettings = z.infer;
diff --git a/packages/structures/src/config.ts b/packages/structures/src/config.ts
index ef29949..3ff76ba 100644
--- a/packages/structures/src/config.ts
+++ b/packages/structures/src/config.ts
@@ -44,9 +44,17 @@ export const ConfigTypeModel = z.record(
);
export type ConfigType = z.infer;
+export const OAuth2ConfigModel = z.object({
+ tokenUrl: z.string().url(),
+ oauthUrl: z.string().url(),
+ scopes: z.string().array()
+});
+export type OAuth2Config = z.infer;
+
export const MailConfigModel = z.object({
type: ConfigTypeModel,
provider: z.string(),
+ oauth2: OAuth2ConfigModel.nullable(),
displayName: z.string()
});
export type MailConfig = z.infer;
diff --git a/packages/structures/src/login.ts b/packages/structures/src/login.ts
index 72b3646..3676232 100644
--- a/packages/structures/src/login.ts
+++ b/packages/structures/src/login.ts
@@ -38,9 +38,35 @@ export const ServerTypeModel = z.union([
]);
export type ServerType = z.infer;
-export const LoginOptionsModel = z.object({
+export const PasswordBasedLoginLiteral = "passwordBased" as const;
+export const PasswordBasedLoginLiteralModel = z.literal(
+ PasswordBasedLoginLiteral
+);
+
+export const OAuthBasedLoginLiteral = "oAuthBased" as const;
+export const OAuthBasedLoginLiteralModel = z.literal(OAuthBasedLoginLiteral);
+
+export const PasswordCredentialsModel = z.object({
+ username: z.string(),
+ password: z.string()
+});
+export type PasswordCredentials = z.infer;
+
+export const OAuthCredentialsModel = z.object({
username: z.string(),
- password: z.string(),
+ accessToken: z.string()
+});
+export type OAuthCredentials = z.infer;
+
+export const LoginTypeModel = z.object({
+ [PasswordBasedLoginLiteral]: PasswordCredentialsModel.optional(),
+ [OAuthBasedLoginLiteral]: OAuthCredentialsModel.optional()
+});
+
+export type LoginType = z.infer;
+
+export const LoginOptionsModel = z.object({
+ loginType: LoginTypeModel,
domain: z.string(),
port: z.number(),
security: ConnectionSecurityModel
@@ -49,6 +75,6 @@ export type LoginOptions = z.infer;
export const CredentialsModel = z.object({
incoming: LoginOptionsModel,
- incoming_type: IncomingMailServerTypeModel
+ incomingType: IncomingMailServerTypeModel
});
export type Credentials = z.infer;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bcc5261..43836c0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -189,9 +189,9 @@ importers:
rollup-plugin-ts: ^3.0.2
rollup-plugin-typescript2: ^0.31.1
typescript: ^4.7.4
- zod: ^3.20.2
+ zod: ^3.21.4
dependencies:
- zod: 3.20.2
+ zod: 3.21.4
devDependencies:
'@dust-mail/tsconfig': link:../tsconfig
'@types/node': 18.7.13
@@ -10117,6 +10117,10 @@ packages:
resolution: {integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==}
dev: false
+ /zod/3.21.4:
+ resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
+ dev: false
+
/zustand/4.0.0_react@18.2.0:
resolution: {integrity: sha512-OrsfQTnRXF1LZ9/vR/IqN9ws5EXUhb149xmPjErZnUrkgxS/gAHGy2dPNIVkVvoxrVe1sIydn4JjF0dYHmGeeQ==}
engines: {node: '>=12.7.0'}