142 lines
4.7 KiB
Rust
142 lines
4.7 KiB
Rust
use std::io::ErrorKind;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use anyhow::{Context, Result};
|
|
use clap::{Parser, Subcommand};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Parser)]
|
|
#[command(version)]
|
|
struct Cli {
|
|
/// Path to a config file.
|
|
///
|
|
/// See https://docs.rs/dirs/5.0.1/dirs/fn.config_dir.html for default
|
|
/// locations. Default location is using the directory from above with a
|
|
/// file name of "duck.toml".
|
|
#[arg(short, long)]
|
|
config: Option<PathBuf>,
|
|
|
|
/// The command to run. If not specified, generates a new Duck address.
|
|
#[command(subcommand)]
|
|
command: Option<Command>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize)]
|
|
struct Config {
|
|
key: String,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Command {
|
|
/// Sets the API key in the config file.
|
|
///
|
|
/// You can get this key from https://duckduckgo.com/email. Open the network
|
|
/// inspector, click generate new address, and copy the Bearer token from
|
|
/// the Authorization header. That's the API key.
|
|
SetKey {
|
|
/// The API key to set in the config file.
|
|
key: String,
|
|
},
|
|
|
|
/// Generate a new Duck address.
|
|
#[command(aliases = ["new", "create", "add"])]
|
|
Generate,
|
|
|
|
/// Deactivates an existing Duck address.
|
|
#[command(aliases = ["disable", "delete", "remove"])]
|
|
Deactivate {
|
|
/// Address to deactivate. This can be just the email username; the
|
|
/// domain is not required.
|
|
address: String,
|
|
},
|
|
|
|
/// Reactivates an existing Duck address.
|
|
#[command(aliases = ["enable", "activate"])]
|
|
Reactivate {
|
|
/// Address to reactivate. This can be just the email username; the
|
|
/// domain is not required.
|
|
address: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
enum Response {
|
|
Address(String),
|
|
Active(bool),
|
|
Error(String),
|
|
}
|
|
|
|
fn config(path: &Path) -> Result<Config> {
|
|
let config = match std::fs::read_to_string(path) {
|
|
Ok(s) => s,
|
|
Err(e) if e.kind() == ErrorKind::NotFound => {
|
|
eprintln!("You are missing a config file at `{}`.", path.to_string_lossy());
|
|
eprintln!("Try running `duck set-key <api key>` first.");
|
|
anyhow::bail!("missing config file");
|
|
}
|
|
Err(e) => Err(e).context("could not read config file")?,
|
|
};
|
|
toml::from_str(&config)
|
|
.context("could not parse config file")
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let args = Cli::parse();
|
|
let command = args.command.unwrap_or(Command::Generate);
|
|
let config_path = args.config.or_else(|| dirs::config_dir().map(|c| c.join("duck.toml")))
|
|
.context("could not find a path for config, try using -c or --config")?;
|
|
|
|
match command {
|
|
Command::SetKey { key } => {
|
|
let config_str = toml::to_string(&Config {
|
|
key,
|
|
}).context("could not serialise config")?;
|
|
std::fs::write(config_path, config_str)
|
|
.context("could not write config")?;
|
|
println!("Config saved.");
|
|
}
|
|
Command::Generate => {
|
|
let config = config(&config_path)?;
|
|
let resp: Response = ureq::post("https://quack.duckduckgo.com/api/email/addresses")
|
|
.set("Authorization", &format!("Bearer {}", config.key))
|
|
.call()
|
|
.context("could not send request")?
|
|
.into_json()
|
|
.context("could not parse response")?;
|
|
match resp {
|
|
Response::Address(a) => println!("{a}@duck.com"),
|
|
Response::Active(_) => anyhow::bail!("unexpected response {resp:#?}"),
|
|
Response::Error(e) => anyhow::bail!("an error occurred: {e}"),
|
|
}
|
|
}
|
|
Command::Deactivate { address } => {
|
|
set_active(&config_path, &address, false)?;
|
|
}
|
|
Command::Reactivate { address } => {
|
|
set_active(&config_path, &address, true)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn set_active(config_path: &Path, address: &str, active: bool) -> Result<()> {
|
|
let config = config(config_path)?;
|
|
let address = address.split('@').next()
|
|
.context("could not get first part of address")?;
|
|
let resp: Response = ureq::put(&format!("https://quack.duckduckgo.com/api/email/addresses/{address}?active={active}"))
|
|
.set("Authorization", &format!("Bearer {}", config.key))
|
|
.call()
|
|
.context("could not send request")?
|
|
.into_json()
|
|
.context("could not parse response")?;
|
|
match resp {
|
|
Response::Address(_) => anyhow::bail!("unexpected response {resp:#?}"),
|
|
Response::Active(a) => println!("{address}@duck.com is now {}", if a { "activated" } else { "deactivated" }),
|
|
Response::Error(e) => anyhow::bail!("an error occurred: {e}"),
|
|
}
|
|
|
|
Ok(())
|
|
}
|