Compare commits

..

4 Commits

Author SHA1 Message Date
holger krekel
2bfe06758b address link2xt comments 2026-07-22 20:59:20 +02:00
holger krekel
0c46959a4c fixup CI failures 2026-07-22 20:53:20 +02:00
holger krekel
fbf28940fd add nauta.cu, hermes.radio and *.aco-connexion.org legacy options 2026-07-22 20:53:20 +02:00
holger krekel
a5cf99244a api!: remove provider-db handling and provider lookup APIs
BREAKING CHANGE: provider lookup APIs were removed from CFFI and JSON-RPC.

also removes offline provider database code and generated provider data,
provider-specific fields in configure/transport paths, and REPL providerinfo.
2026-07-22 20:52:20 +02:00
26 changed files with 175 additions and 3655 deletions

View File

@@ -68,20 +68,6 @@ jobs:
command: check
command-arguments: "-Dwarnings"
provider_database:
name: Check provider database
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
with:
show-progress: false
persist-credentials: false
- name: Install rustfmt
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
- name: Check provider database
run: scripts/update-provider-database.sh
docs:
name: Rust doc comments
runs-on: ubuntu-latest

View File

@@ -19,7 +19,6 @@ typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_event_channel dc_event_channel_t;
@@ -5200,98 +5199,6 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
uint32_t dc_contact_get_verifier_id (dc_contact_t* contact);
/**
* @class dc_provider_t
*
* Opaque object containing information about one single e-mail provider.
*/
/**
* Create a provider struct for the given e-mail address by local lookup.
*
* Lookup is done from the local database by extracting the domain from the e-mail address.
* Therefore the provider for custom domains cannot be identified.
*
* @memberof dc_provider_t
* @param context The context object.
* @param email The user's e-mail address to extract the provider info form.
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_email (const dc_context_t* context, const char* email);
/**
* Create a provider struct for the given e-mail address by local lookup.
*
* DNS lookup is not used anymore and this function is deprecated.
*
* @memberof dc_provider_t
* @param context The context object.
* @param email The user's e-mail address to extract the provider info form.
* @return A dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
* @deprecated 2025-10-17 use dc_provider_new_from_email() instead.
*/
dc_provider_t* dc_provider_new_from_email_with_dns (const dc_context_t* context, const char* email);
/**
* URL of the overview page.
*
* This URL allows linking to the providers page on providers.delta.chat.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string with a fully-qualified URL,
* if there is no such URL, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
*/
char* dc_provider_get_overview_page (const dc_provider_t* provider);
/**
* Get hints to be shown to the user on the login screen.
* Depending on the @ref DC_PROVIDER_STATUS returned by dc_provider_get_status(),
* the UI may want to highlight the hint.
*
* Moreover, the UI should display a "More information" link
* that forwards to the URL returned by dc_provider_get_overview_page().
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string with the hint to show to the user, may contain multiple lines,
* if there is no such hint, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
*/
char* dc_provider_get_before_login_hint (const dc_provider_t* provider);
/**
* Whether DC works with this provider.
*
* Can be one of #DC_PROVIDER_STATUS_OK,
* #DC_PROVIDER_STATUS_PREPARATION or #DC_PROVIDER_STATUS_BROKEN.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return The status as a constant number.
*/
int dc_provider_get_status (const dc_provider_t* provider);
/**
* Free the provider info struct.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
*/
void dc_provider_unref (dc_provider_t* provider);
/**
* @class dc_lot_t
*
@@ -6612,61 +6519,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_MEDIA_QUALITY_WORSE 1
/**
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
*
* These constants are used as return values for dc_provider_get_status().
*
* @addtogroup DC_PROVIDER_STATUS
* @{
*/
/**
* Provider works out-of-the-box.
* This provider status is returned for provider where the login
* works by just entering the name or the e-mail address.
*
* - There is no need for the user to do any special things
* (enable IMAP or so) in the provider's web interface or at other places.
* - There is no need for the user to enter advanced settings;
* server, port etc. are known by the core.
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_OK 1
/**
* Provider works, but there are preparations needed.
*
* - The user has to do some special things as "Enable IMAP in the web interface",
* what exactly, is described in the string returned by dc_provider_get_before_login_hints()
* and, typically more detailed, in the page linked by dc_provider_get_overview_page().
* - There is no need for the user to enter advanced settings;
* server, port etc. should be known by the core.
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_PREPARATION 2
/**
* Provider is not working.
* This provider status is returned for providers
* that are known to not work with Delta Chat.
* The UI should block logging in with this provider.
*
* More information about that is typically provided
* in the string returned by dc_provider_get_before_login_hints()
* and in the page linked by dc_provider_get_overview_page().
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_BROKEN 3
/**
* @}
*/
/**
* @defgroup DC_CHAT_VISIBILITY DC_CHAT_VISIBILITY
*

View File

@@ -4523,99 +4523,6 @@ fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> V
msg_ids
}
// dc_provider_t
pub type dc_provider_t = provider::Provider;
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email(
context: *const dc_context_t,
addr: *const libc::c_char,
) -> *const dc_provider_t {
if context.is_null() || addr.is_null() {
eprintln!("ignoring careless call to dc_provider_new_from_email()");
return ptr::null();
}
let addr = to_string_lossy(addr);
let ctx = &*context;
match provider::get_provider_info_by_addr(addr.as_str())
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
context: *const dc_context_t,
addr: *const libc::c_char,
) -> *const dc_provider_t {
if context.is_null() || addr.is_null() {
eprintln!("ignoring careless call to dc_provider_new_from_email_with_dns()");
return ptr::null();
}
let addr = to_string_lossy(addr);
let ctx = &*context;
match provider::get_provider_info_by_addr(addr.as_str())
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_overview_page(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_overview_page()");
return "".strdup();
}
let provider = &*provider;
provider.overview_page.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_before_login_hint(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_before_login_hint()");
return "".strdup();
}
let provider = &*provider;
provider.before_login_hint.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> libc::c_int {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_status()");
return 0;
}
let provider = &*provider;
provider.status as libc::c_int
}
#[no_mangle]
#[allow(clippy::needless_return)]
pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_unref()");
return;
}
// currently, there is nothing to free, the provider info is a static object.
// this may change once we start localizing string.
}
// -- Accounts
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using

View File

@@ -29,7 +29,6 @@ use deltachat::message::{
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
};
use deltachat::provider::get_provider_info;
use deltachat::qr::{self, Qr};
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
@@ -55,7 +54,6 @@ use types::events::Event;
use types::http::HttpResponse;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
use types::notify_state::JsonrpcNotifyState;
use types::provider_info::ProviderInfo;
use types::reactions::JsonrpcReactions;
use types::webxdc::WebxdcMessageInfo;
@@ -345,21 +343,6 @@ impl CommandApi {
Ok(dbfile + total_size)
}
/// Returns provider for the given domain.
///
/// This function looks up domain in offline database.
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
async fn get_provider_info(
&self,
_account_id: u32,
email: String,
) -> Result<Option<ProviderInfo>> {
let provider_info = get_provider_info(email.split('@').next_back().unwrap_or(""));
Ok(ProviderInfo::from_dc_type(provider_info))
}
/// Checks if the context is already configured.
async fn is_configured(&self, account_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;

View File

@@ -168,9 +168,8 @@ impl From<Socket> for dc::Socket {
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum EnteredCertificateChecks {
/// `Automatic` means that provider database setting should be taken.
/// If there is no provider database setting for certificate checks,
/// check certificates strictly.
/// `Automatic` means strict certificate checks,
/// unless a legacy-domain override disables them.
#[default]
Automatic,

View File

@@ -9,7 +9,7 @@ pub mod location;
pub mod login_param;
pub mod message;
pub mod notify_state;
pub mod provider_info;
pub mod qr;
pub mod reactions;
pub mod webxdc;

View File

@@ -1,25 +0,0 @@
use deltachat::provider::Provider;
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProviderInfo {
/// Unique ID, corresponding to provider database filename.
pub id: String,
pub before_login_hint: String,
pub overview_page: String,
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
}
impl ProviderInfo {
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
provider.map(|p| ProviderInfo {
id: p.id.to_owned(),
before_login_hint: p.before_login_hint.to_owned(),
overview_page: p.overview_page.to_owned(),
status: p.status.to_u32().unwrap(),
})
}
}

View File

@@ -148,23 +148,6 @@ describe("online tests", function () {
expect(message2.text).equal("super secret message");
expect(message2.showPadlock).equal(true);
});
it("get provider info for example.com", async () => {
const acc = await dc.rpc.addAccount();
const info = await dc.rpc.getProviderInfo(acc, "example.com");
expect(info).to.be.not.null;
expect(info?.overviewPage).to.equal(
"https://providers.delta.chat/example-com",
);
expect(info?.status).to.equal(3);
});
it("get provider info - domain and email should give same result", async () => {
const acc = await dc.rpc.addAccount();
const info_domain = await dc.rpc.getProviderInfo(acc, "example.com");
const info_email = await dc.rpc.getProviderInfo(acc, "hi@example.com");
expect(info_email).to.deep.equal(info_domain);
});
});
async function waitForEvent<T extends DcEvent["kind"]>(

View File

@@ -8,6 +8,7 @@ use std::time::Duration;
use anyhow::{bail, ensure, Result};
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
@@ -24,7 +25,6 @@ use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
use deltachat::tools::*;
use deltachat::{config, provider};
use tokio::fs;
/// Reset database tables.
@@ -395,7 +395,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
joinqr <qr-content>\n\
setqr <qr-content>\n\
createqrsvg <qr-content>\n\
providerinfo <addr>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
clear -- clear screen\n\
@@ -1204,24 +1203,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
fs::write(&file, svg).await?;
println!("{file:#?} written.");
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {arg1}:");
println!("status: {}", info.status as u32);
println!("before_login_hint: {}", info.before_login_hint);
println!("after_login_hint: {}", info.after_login_hint);
println!("overview_page: {}", info.overview_page);
for server in info.server.iter() {
println!("server: {}:{}", server.hostname, server.port,);
}
}
None => {
println!("No information for provider belonging to {arg1} found.");
}
}
}
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");

View File

@@ -237,7 +237,7 @@ const CONTACT_COMMANDS: [&str; 9] = [
"import-vcard",
"make-vcard",
];
const MISC_COMMANDS: [&str; 14] = [
const MISC_COMMANDS: [&str; 13] = [
"getqr",
"getqrsvg",
"getbadqr",
@@ -245,7 +245,6 @@ const MISC_COMMANDS: [&str; 14] = [
"joinqr",
"setqr",
"createqrsvg",
"providerinfo",
"fileinfo",
"estimatedeletion",
"clear",

View File

@@ -747,28 +747,6 @@ def test_early_failure(tmp_path) -> None:
rpc.start()
def test_provider_info(rpc) -> None:
account_id = rpc.add_account()
provider_info = rpc.get_provider_info(account_id, "example.org")
assert provider_info["id"] == "example.com"
provider_info = rpc.get_provider_info(account_id, "uep7oiw4ahtaizuloith.org")
assert provider_info is None
# Test MX record resolution.
# This previously resulted in Gmail provider
# because MX record pointed to google.com domain,
# but MX record resolution has been removed.
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
# Disable MX record resolution.
rpc.set_config(account_id, "proxy_enabled", "1")
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)

View File

@@ -1,46 +0,0 @@
"""Provider info class."""
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer
class ProviderNotFoundError(Exception):
"""The provider information was not found."""
class Provider:
"""
Provider information.
:param domain: The email to get the provider info for.
"""
def __init__(self, account, addr) -> None:
provider = ffi.gc(
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
lib.dc_provider_unref,
)
if provider == ffi.NULL:
raise ProviderNotFoundError("Provider not found")
self._provider = provider
@property
def overview_page(self) -> str:
"""URL to the overview page of the provider on providers.delta.chat."""
return from_dc_charpointer(lib.dc_provider_get_overview_page(self._provider))
@property
def get_before_login_hints(self) -> str:
"""Should be shown to the user on login."""
return from_dc_charpointer(lib.dc_provider_get_before_login_hint(self._provider))
@property
def status(self) -> int:
"""The status of the provider information.
This is one of the
:attr:`deltachat.const.DC_PROVIDER_STATUS_OK`,
:attr:`deltachat.const.DC_PROVIDER_STATUS_PREPARATION` or
:attr:`deltachat.const.DC_PROVIDER_STATUS_BROKEN` constants.
"""
return lib.dc_provider_get_status(self._provider)

View File

@@ -171,14 +171,6 @@ def test_get_special_message_id_returns_empty_message(acfactory):
assert msg.id == 0
def test_provider_info_none():
ctx = ffi.gc(
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_open(tmp_path):
db_fname = tmp_path / "test.db"
ctx = ffi.gc(

View File

@@ -1,242 +0,0 @@
#!/usr/bin/env python3
# if the yaml import fails, run "pip install pyyaml"
import sys
import yaml
import datetime
from pathlib import Path
out_all = ""
out_domains = ""
out_ids = ""
domains_set = set()
def camel(name):
words = name.split("_")
return "".join(w.capitalize() for i, w in enumerate(words))
def cleanstr(s):
s = s.strip()
s = s.replace("\n", " ")
s = s.replace("\\", "\\\\")
s = s.replace('"', '\\"')
return s
def file2id(f):
return f.stem
def file2varname(f):
f = file2id(f)
f = f.replace(".", "_")
f = f.replace("-", "_")
return "P_" + f.upper()
def file2url(f):
f = file2id(f)
f = f.replace(".", "-")
return "https://providers.delta.chat/" + f
def process_opt(data):
if not "opt" in data:
return "ProviderOptions::new()"
opt = "ProviderOptions {\n"
opt_data = data.get("opt", "")
for key in opt_data:
value = str(opt_data[key])
if key == "max_smtp_rcpt_to":
value = "Some(" + value + ")"
if value in {"True", "False"}:
value = value.lower()
opt += " " + key + ": " + value + ",\n"
opt += " ..ProviderOptions::new()\n"
opt += " }"
return opt
def process_config_defaults(data):
if not "config_defaults" in data:
return "None"
defaults = "Some(&[\n"
config_defaults = data.get("config_defaults", "")
for key in config_defaults:
value = str(config_defaults[key])
defaults += (
" ConfigDefault { key: Config::"
+ camel(key)
+ ', value: "'
+ value
+ '" },\n'
)
defaults += " ])"
return defaults
def process_data(data, file):
status = data.get("status", "")
if status != "OK" and status != "PREPARATION" and status != "BROKEN":
raise TypeError("bad status")
comment = ""
domains = ""
if not "domains" in data:
raise TypeError("no domains found")
for domain in data["domains"]:
domain = cleanstr(domain)
if domain == "" or domain.lower() != domain:
raise TypeError("bad domain: " + domain)
global domains_set
if domain in domains_set:
raise TypeError("domain used twice: " + domain)
domains_set.add(domain)
domains += ' ("' + domain + '", &' + file2varname(file) + "),\n"
comment += domain + ", "
ids = ""
ids += ' ("' + file2id(file) + '", &' + file2varname(file) + "),\n"
server = ""
has_imap = False
has_smtp = False
if "server" in data:
for s in data["server"]:
hostname = cleanstr(s.get("hostname", ""))
port = int(s.get("port", ""))
if hostname == "" or hostname.lower() != hostname or port <= 0:
raise TypeError("bad hostname or port")
protocol = s.get("type", "").upper()
if protocol == "IMAP":
has_imap = True
elif protocol == "SMTP":
has_smtp = True
else:
raise TypeError("bad protocol")
socket = s.get("socket", "").upper()
if socket != "STARTTLS" and socket != "SSL" and socket != "PLAIN":
raise TypeError("bad socket")
username_pattern = s.get("username_pattern", "EMAIL").upper()
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
raise TypeError("bad username pattern")
server += (
" Server { protocol: "
+ protocol.capitalize()
+ ", socket: "
+ socket.capitalize()
+ ', hostname: "'
+ hostname
+ '", port: '
+ str(port)
+ ", username_pattern: "
+ username_pattern.capitalize()
+ " },\n"
)
opt = process_opt(data)
config_defaults = process_config_defaults(data)
provider = ""
before_login_hint = cleanstr(data.get("before_login_hint", "") or "")
after_login_hint = cleanstr(data.get("after_login_hint", ""))
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
provider += (
"static "
+ file2varname(file)
+ ": Provider = Provider {\n"
)
provider += ' id: "' + file2id(file) + '",\n'
provider += " status: Status::" + status.capitalize() + ",\n"
provider += ' before_login_hint: "' + before_login_hint + '",\n'
provider += ' after_login_hint: "' + after_login_hint + '",\n'
provider += ' overview_page: "' + file2url(file) + '",\n'
provider += " server: &[\n" + server + " ],\n"
provider += " opt: " + opt + ",\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += "};\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")
if status != "OK" and before_login_hint == "":
raise TypeError(
"status PREPARATION or BROKEN requires before_login_hint: " + file
)
# finally, add the provider
global out_all, out_domains, out_ids
out_all += "// " + file.name + ": " + comment.strip(", ") + "\n"
# also add provider with no special things to do -
# eg. we can skip the mx-lookup in this case
out_all += provider
out_domains += domains
out_ids += ids
def process_file(file):
print("processing file: {}".format(file), file=sys.stderr)
with open(file) as f:
# load_all() loads "---"-separated yamls -
# by coincidence, this is also the frontmatter separator :)
data = next(yaml.load_all(f, Loader=yaml.SafeLoader))
process_data(data, file)
def process_dir(dir):
print("processing directory: {}".format(dir), file=sys.stderr)
files = sorted(f for f in dir.iterdir() if f.suffix == ".md")
for f in files:
process_file(f)
if __name__ == "__main__":
if len(sys.argv) < 2:
raise SystemExit("usage: update.py DIR_WITH_MD_FILES > data.rs")
out_all = (
"// file generated by src/provider/update.py\n\n"
"use crate::provider::Protocol::*;\n"
"use crate::provider::Socket::*;\n"
"use crate::provider::UsernamePattern::*;\n"
"use crate::provider::{\n"
" Config, ConfigDefault, Provider, ProviderOptions, Server, Status,\n"
"};\n"
"use std::collections::HashMap;\n\n"
"use std::sync::LazyLock;\n\n"
)
process_dir(Path(sys.argv[1]))
out_all += "pub(crate) static PROVIDER_DATA: [(&str, &Provider); " + str(len(domains_set)) + "] = [\n";
out_all += out_domains
out_all += "];\n\n"
out_all += "pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider>> = LazyLock::new(|| HashMap::from([\n"
out_all += out_ids
out_all += "]));\n\n"
if len(sys.argv) < 3:
now = datetime.datetime.utcnow()
else:
now = datetime.datetime.fromisoformat(sys.argv[2])
out_all += (
"pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> = "
"LazyLock::new(|| chrono::NaiveDate::from_ymd_opt("
+ str(now.year)
+ ", "
+ str(now.month)
+ ", "
+ str(now.day)
+ ").unwrap());\n"
)
print(out_all)

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env bash
# Updates provider database.
# Returns 1 if the database is changed, 0 otherwise.
set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=2cba4b72f4c6e6417b83ba549aff7781be5f166c
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"
git clone --filter=blob:none https://github.com/chatmail/provider-db.git "$TMP"
cd "$TMP"
git checkout "$REV"
DATE=$(git show -s --format=%cs)
"$CORE_ROOT"/scripts/create-provider-data-rs.py "$TMP/_providers" "$DATE" >"$CORE_ROOT/src/provider/data.rs"
rustfmt --edition 2024 "$CORE_ROOT/src/provider/data.rs"
rm -fr "$TMP"
cd "$CORE_ROOT"
test -z "$(git status --porcelain src/provider/data.rs)"

View File

@@ -294,9 +294,6 @@ pub enum Config {
/// Unix timestamp of the last successful configuration.
ConfiguredTimestamp,
/// ID of the configured provider from the provider database.
ConfiguredProvider,
/// Deprecated(2026-04).
/// Use [`Context::is_configured()`] instead.
///

View File

@@ -1,11 +1,9 @@
//! # Email accounts autoconfiguration process.
//!
//! The module provides automatic lookup of configuration
//! for email providers based on the built-in [provider database],
//! [Mozilla Thunderbird Autoconfiguration protocol]
//! The module provides automatic lookup of configuration for email providers
//! using [Mozilla Thunderbird Autoconfiguration protocol]
//! and [Outlook's Autodiscover].
//!
//! [provider database]: crate::provider
//! [Mozilla Thunderbird Autoconfiguration protocol]: auto_mozilla
//! [Outlook's Autodiscover]: auto_outlook
@@ -30,19 +28,17 @@ use crate::imap::Imap;
use crate::log::warn;
pub use crate::login_param::EnteredLoginParam;
use crate::login_param::{EnteredCertificateChecks, TransportListEntry};
use crate::message::Message;
use crate::net::proxy::ProxyConfig;
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
use crate::provider::{self, Protocol, Socket};
use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
use crate::smtp::Smtp;
use crate::sync::Sync::*;
use crate::sync::Sync::Nosync;
use crate::tools::time;
use crate::transport::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, send_sync_transports,
};
use crate::{EventType, stock_str};
use crate::{chat, provider};
/// Maximum number of relays.
///
@@ -339,25 +335,22 @@ impl Context {
self.try_make_space_for_new_relay().await?;
}
let provider = match configure(self, param).await {
Err(error) => {
// Log entered and actual params
let configured_param = get_configured_param(self, param).await;
warn!(
self,
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
param.to_string(),
configured_param
.map(|param| param.to_string())
.unwrap_or("error".to_owned())
);
return Err(error);
}
Ok(provider) => provider,
if let Err(error) = configure(self, param).await {
// Log entered and actual params
let configured_param = get_configured_param(self, param).await;
warn!(
self,
"configure failed: Entered params: {}. Used params: {}. Error: {error}.",
param.to_string(),
configured_param
.map(|param| param.to_string())
.unwrap_or("error".to_owned())
);
return Err(error);
};
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, provider).await?;
apply_legacy_domain_config_defaults(self, &param.addr).await?;
Ok(())
}
@@ -398,42 +391,25 @@ impl Context {
}
}
async fn on_configure_completed(
context: &Context,
provider: Option<&'static Provider>,
) -> Result<()> {
if let Some(provider) = provider {
if let Some(config_defaults) = provider.config_defaults {
for def in config_defaults {
if !context.config_exists(def.key).await? {
info!(context, "apply config_defaults {}={}", def.key, def.value);
context
.set_config_ex(Nosync, def.key, Some(def.value))
.await?;
} else {
info!(
context,
"skip already set config_defaults {}={}", def.key, def.value
);
}
}
}
/// Applies a few select non-default config values that used to come from provider database.
async fn apply_legacy_domain_config_defaults(context: &Context, addr: &str) -> Result<()> {
let settings = provider::legacy_settings_for_addr(addr);
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new_text(provider.after_login_hint.to_string());
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
.await
.is_err()
{
warn!(context, "cannot add after_login_hint as core-provider-info");
}
}
if settings.disable_mdns && !context.config_exists(Config::MdnsEnabled).await? {
context
.set_config_ex(Nosync, Config::MdnsEnabled, Some("0"))
.await?;
}
if settings.worse_media_quality && !context.config_exists(Config::MediaQuality).await? {
context
.set_config_ex(Nosync, Config::MediaQuality, Some("1"))
.await?;
}
Ok(())
}
/// Retrieves data from autoconfig and provider database
/// Retrieves data from autoconfig
/// to transform user-entered login parameters into complete configuration.
async fn get_configured_param(
ctx: &Context,
@@ -457,9 +433,7 @@ async fn get_configured_param(
progress!(ctx, 200);
let provider;
let param_autoconfig;
if param.imap.server.is_empty()
let param_autoconfig = if param.imap.server.is_empty()
&& param.imap.port == 0
&& param.imap.security == Socket::Automatic
&& param.imap.user.is_empty()
@@ -468,51 +442,17 @@ async fn get_configured_param(
&& param.smtp.security == Socket::Automatic
&& param.smtp.user.is_empty()
{
// no advanced parameters entered by the user: query provider-database or do Autoconfig
info!(
ctx,
"checking internal provider-info for offline autoconfig"
);
provider = provider::get_provider_info(&param_domain);
if let Some(provider) = provider {
if provider.server.is_empty() {
info!(ctx, "Offline autoconfig found, but no servers defined.");
param_autoconfig = None;
} else {
info!(ctx, "Offline autoconfig found.");
let servers = provider
.server
.iter()
.map(|s| ServerParams {
protocol: s.protocol,
socket: s.socket,
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::Email => param.addr.to_string(),
UsernamePattern::Emaillocalpart => {
if let Some(at) = param.addr.find('@') {
param.addr.split_at(at).0.to_string()
} else {
param.addr.to_string()
}
}
},
})
.collect();
param_autoconfig = Some(servers)
}
// no advanced parameters entered by the user: do Autoconfig
// except for a few known legacy-domain overrides.
let legacy_servers = provider::legacy_settings_for_addr(&param.addr).autoconfig_servers;
if legacy_servers.is_some() {
legacy_servers
} else {
// Try receiving autoconfig
info!(ctx, "No offline autoconfig found.");
param_autoconfig = get_autoconfig(ctx, param, &param_domain).await;
get_autoconfig(ctx, param, &param_domain).await
}
} else {
provider = None;
param_autoconfig = None;
}
None
};
progress!(ctx, 500);
@@ -591,7 +531,6 @@ async fn get_configured_param(
.collect(),
smtp_user: param.smtp.user.clone(),
smtp_password,
provider,
certificate_checks: match param.certificate_checks {
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
@@ -604,7 +543,7 @@ async fn get_configured_param(
Ok(configured_login_param)
}
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<()> {
progress!(ctx, 1);
let configured_param = get_configured_param(ctx, param).await?;
@@ -674,7 +613,6 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
progress!(ctx, 910);
let provider = configured_param.provider;
configured_param
.clone()
.save_to_transports_table(ctx, param, time())
@@ -696,7 +634,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.emit_event(EventType::AccountsItemChanged);
Ok(provider)
Ok(())
}
/// Retrieve available autoconfigurations.

View File

@@ -1,10 +1,10 @@
//! Variable server parameters lists
use crate::provider::{Protocol, Socket};
pub use crate::provider::{Protocol, Socket};
/// Set of variable parameters to try during configuration.
///
/// Can be loaded from offline provider database, online configuration
/// Can be loaded from online configuration
/// or derived from user entered parameters.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ServerParams {
@@ -302,8 +302,7 @@ mod tests {
// as this is very uncommon configuration
// and not worth doubling the number of candidates to try.
// If such configuration is used, email provider
// should provide XML autoconfig or
// be added to the provider database as an exception.
// should provide XML autoconfig.
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Imap,

View File

@@ -586,11 +586,11 @@ impl Context {
if let Some(limit) = metadata_limit {
return Ok(limit);
}
let val = param
.provider
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, u32::from);
Ok(val)
if let Some(limit) = crate::provider::legacy_settings_for_addr(&param.addr).max_smtp_rcpt_to
{
return Ok(limit);
}
Ok(constants::DEFAULT_MAX_SMTP_RCPT_TO)
}
/// Does a single round of fetching from IMAP and returns.

View File

@@ -37,9 +37,8 @@ use crate::tools::ToOption;
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub enum EnteredCertificateChecks {
/// `Automatic` means that provider database setting should be taken.
/// If there is no provider database setting for certificate checks,
/// check certificates strictly.
/// `Automatic` means strict certificate checks,
/// unless a legacy-domain override disables them.
#[default]
Automatic = 0,

View File

@@ -1,28 +1,9 @@
//! [Provider database](https://providers.delta.chat/) module.
//! Provider types.
pub(crate) mod data;
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;
use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
/// Provider status according to manual testing.
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Status {
/// Provider is known to be working with Delta Chat.
Ok = 1,
/// Provider works with Delta Chat, but requires some preparation,
/// such as changing the settings in the web interface.
Preparation = 2,
/// Provider is known not to work with Delta Chat.
Broken = 3,
}
use crate::configure::server_params::ServerParams;
/// Server protocol.
#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
@@ -76,118 +57,79 @@ pub enum UsernamePattern {
Emaillocalpart = 2,
}
/// Email server endpoint.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Server {
/// Server protocol, e.g. SMTP or IMAP.
pub protocol: Protocol,
/// Port security, e.g. TLS or STARTTLS.
pub socket: Socket,
/// Server host.
pub hostname: &'static str,
/// Server port.
pub port: u16,
/// Pattern used to construct login usernames from email addresses.
pub username_pattern: UsernamePattern,
/// Returns true if `domain` is `suffix` itself or a subdomain of it.
fn is_exact_or_subdomain(domain: &str, suffix: &str) -> bool {
domain == suffix || domain.ends_with(&format!(".{suffix}"))
}
/// Pair of key and value for default configuration.
#[derive(Debug, PartialEq, Eq)]
pub struct ConfigDefault {
/// Configuration variable name.
pub key: Config,
/// Non-default settings that used to be looked up in provider.db for a few domains.
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct LegacyProviderSettings {
/// Servers to use instead of autoconfig, if any.
pub autoconfig_servers: Option<Vec<ServerParams>>,
/// Configuration variable value.
pub value: &'static str,
/// Maximum number of recipients allowed in a single SMTP send, if limited.
pub max_smtp_rcpt_to: Option<u32>,
/// Whether to disable strict TLS certificate checks by default.
pub disable_strict_tls: bool,
/// Whether to disable local network contact discovery (mDNS) by default.
pub disable_mdns: bool,
/// Whether to default to worse media quality (for slow/expensive connections).
pub worse_media_quality: bool,
}
/// Provider database entry.
#[derive(Debug, PartialEq, Eq)]
pub struct Provider {
/// Unique ID, corresponding to provider database filename.
pub id: &'static str,
/// Provider status according to manual testing.
pub status: Status,
/// Hint to be shown to the user on the login screen.
pub before_login_hint: &'static str,
/// Hint to be added to the device chat after provider configuration.
pub after_login_hint: &'static str,
/// URL of the page with provider overview.
pub overview_page: &'static str,
/// List of provider servers.
pub server: &'static [Server],
/// Default configuration values to set when provider is configured.
pub config_defaults: Option<&'static [ConfigDefault]>,
/// Options with good defaults.
pub opt: ProviderOptions,
}
/// Provider options with good defaults.
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ProviderOptions {
/// True if provider is known to use use proper,
/// not self-signed certificates.
pub strict_tls: bool,
/// Maximum number of recipients the provider allows to send a single email to.
pub max_smtp_rcpt_to: Option<u16>,
}
impl ProviderOptions {
const fn new() -> Self {
Self {
strict_tls: true,
max_smtp_rcpt_to: None,
}
}
}
/// Returns provider for the given an e-mail address.
/// Returns hard-coded legacy settings for the domain of `addr`.
///
/// Returns an error if provided address is not valid.
pub fn get_provider_info_by_addr(addr: &str) -> Result<Option<&'static Provider>> {
let addr = EmailAddress::new(addr)?;
/// Provider.db lookup was removed, but a handful of domains still need these overrides,
/// so they are hard-coded here instead.
pub(crate) fn legacy_settings_for_addr(addr: &str) -> LegacyProviderSettings {
let Ok(email) = EmailAddress::new(addr) else {
return LegacyProviderSettings::default();
};
let domain = email.domain.to_ascii_lowercase();
Ok(get_provider_info(&addr.domain))
}
/// Finds a provider in offline database based on domain.
pub fn get_provider_info(domain: &str) -> Option<&'static Provider> {
let domain = domain.to_lowercase();
for (pattern, provider) in PROVIDER_DATA {
if let Some(suffix) = pattern.strip_prefix('*') {
// Wildcard domain pattern.
//
// For example, `suffix` is ".hermes.radio" for "*.hermes.radio" pattern.
if domain.ends_with(suffix) {
return Some(provider);
match domain.as_str() {
"nauta.cu" => LegacyProviderSettings {
autoconfig_servers: Some(vec![
ServerParams {
protocol: Protocol::Imap,
socket: Socket::Starttls,
hostname: "imap.nauta.cu".to_string(),
port: 143,
username: String::new(),
},
ServerParams {
protocol: Protocol::Smtp,
socket: Socket::Starttls,
hostname: "smtp.nauta.cu".to_string(),
port: 25,
username: String::new(),
},
]),
max_smtp_rcpt_to: Some(20),
disable_strict_tls: true,
worse_media_quality: true,
..Default::default()
},
_ if is_exact_or_subdomain(&domain, "hermes.radio")
|| domain.ends_with(".aco-connexion.org") =>
{
LegacyProviderSettings {
disable_strict_tls: true,
disable_mdns: true,
..Default::default()
}
} else if pattern == domain {
return Some(provider);
}
}
None
}
/// Returns a provider with the given ID from the database.
pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
if let Some(provider) = PROVIDER_IDS.get(id) {
Some(provider)
} else {
None
_ => LegacyProviderSettings {
autoconfig_servers: None,
max_smtp_rcpt_to: None,
disable_strict_tls: false,
disable_mdns: false,
worse_media_quality: false,
},
}
}
@@ -196,58 +138,39 @@ mod tests {
use super::*;
#[test]
fn test_get_provider_by_domain_unexistant() {
let provider = get_provider_info("unexistant.org");
assert!(provider.is_none());
}
fn test_legacy_domain_overrides() {
let nauta = legacy_settings_for_addr("alice@nauta.cu");
assert_eq!(nauta.max_smtp_rcpt_to, Some(20));
assert!(nauta.disable_strict_tls);
assert!(nauta.worse_media_quality);
let servers = nauta.autoconfig_servers.unwrap();
assert_eq!(servers.len(), 2);
assert_eq!(servers[0].hostname, "imap.nauta.cu");
assert_eq!(servers[1].hostname, "smtp.nauta.cu");
#[test]
fn test_get_provider_by_domain_mixed_case() {
let provider = get_provider_info("nAUta.Cu").unwrap();
assert!(provider.status == Status::Ok);
}
let hermes = legacy_settings_for_addr("alice@foo.hermes.radio");
assert!(hermes.disable_strict_tls);
assert!(hermes.disable_mdns);
// hermes.radio itself (not just its subdomains) is also a valid provider domain.
assert!(legacy_settings_for_addr("alice@hermes.radio").disable_strict_tls);
#[test]
fn test_get_provider_info() {
let addr = "nauta.cu";
let provider = get_provider_info(addr).unwrap();
assert!(provider.status == Status::Ok);
let server = &provider.server[0];
assert_eq!(server.protocol, Protocol::Imap);
assert_eq!(server.socket, Socket::Starttls);
assert_eq!(server.hostname, "imap.nauta.cu");
assert_eq!(server.port, 143);
assert_eq!(server.username_pattern, UsernamePattern::Email);
let server = &provider.server[1];
assert_eq!(server.protocol, Protocol::Smtp);
assert_eq!(server.socket, Socket::Starttls);
assert_eq!(server.hostname, "smtp.nauta.cu");
assert_eq!(server.port, 25);
assert_eq!(server.username_pattern, UsernamePattern::Email);
let aco = legacy_settings_for_addr("alice@foo.aco-connexion.org");
assert!(aco.disable_strict_tls);
assert!(aco.disable_mdns);
// Unlike hermes.radio, aco-connexion.org itself is not a valid provider domain,
// only its subdomains are (matching the original provider.db entries).
let not_aco = legacy_settings_for_addr("alice@aco-connexion.org");
assert_eq!(not_aco.autoconfig_servers, None);
assert_eq!(not_aco.max_smtp_rcpt_to, None);
assert!(!not_aco.disable_strict_tls);
assert!(!not_aco.disable_mdns);
assert!(!not_aco.worse_media_quality);
let provider = get_provider_info("gmail.com").unwrap();
assert!(provider.status == Status::Preparation);
assert!(!provider.before_login_hint.is_empty());
assert!(!provider.overview_page.is_empty());
let provider = get_provider_info("googlemail.com").unwrap();
assert!(provider.status == Status::Preparation);
assert!(get_provider_info("").is_none());
assert!(get_provider_info("google.com").unwrap().id == "gmail");
assert!(get_provider_info("example@google.com").is_none());
}
#[test]
fn test_get_provider_by_id() {
let provider = get_provider_by_id("gmail").unwrap();
assert!(provider.id == "gmail");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_provider_info_by_addr() -> Result<()> {
assert!(get_provider_info_by_addr("google.com").is_err());
assert!(get_provider_info_by_addr("example@google.com")?.unwrap().id == "gmail");
Ok(())
let unknown = legacy_settings_for_addr("alice@example.org");
assert_eq!(unknown.autoconfig_servers, None);
assert_eq!(unknown.max_smtp_rcpt_to, None);
assert!(!unknown.disable_strict_tls);
assert!(!unknown.disable_mdns);
assert!(!unknown.worse_media_quality);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,10 @@ use anyhow::{Context as _, Result, bail};
use deltachat_contact_tools::may_be_valid_addr;
use super::{DCLOGIN_SCHEME, Qr};
use crate::configure::server_params::Socket;
use crate::login_param::{
EnteredCertificateChecks, EnteredImapLoginParam, EnteredLoginParam, EnteredSmtpLoginParam,
};
use crate::provider::Socket;
/// Options for `dclogin:` scheme.
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@@ -5,7 +5,6 @@ use std::collections::BTreeSet;
use std::time::Duration;
use anyhow::{Context as _, Result, ensure};
use deltachat_contact_tools::EmailAddress;
use deltachat_contact_tools::addr_cmp;
use pgp::composed::SignedPublicKey;
use rusqlite::OptionalExtension;
@@ -15,7 +14,7 @@ use crate::configure::EnteredLoginParam;
use crate::context::Context;
use crate::key::DcKey;
use crate::log::warn;
use crate::provider::get_provider_info;
use crate::sql::Sql;
use crate::tools::{self, Time, inc_and_check, time_elapsed};
use crate::transport::ConfiguredLoginParam;
@@ -1118,22 +1117,7 @@ UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
.await?;
}
if dbversion < 71 {
if let Ok(addr) = context.get_primary_self_addr().await {
if let Ok(domain) = EmailAddress::new(&addr).map(|email| email.domain) {
context
.set_config_internal(
Config::ConfiguredProvider,
get_provider_info(&domain).map(|provider| provider.id),
)
.await?;
} else {
warn!(context, "Can't parse configured address: {:?}", addr);
}
}
sql.set_db_version(71).await?;
}
// Migration 71 was removed together with the provider database it read from.
if dbversion < 72 && !sql.col_exists("msgs", "mime_modified").await? {
sql.execute_migration(
r#"

View File

@@ -16,13 +16,12 @@ use deltachat_contact_tools::{EmailAddress, addr_normalize};
use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::configure::server_params::{ServerParams, expand_param_vector};
use crate::context::Context;
use crate::ensure_and_debug_assert;
use crate::events::EventType;
use crate::login_param::EnteredLoginParam;
use crate::net::load_connection_timestamp;
use crate::provider::{Protocol, Provider, Socket, UsernamePattern, get_provider_by_id};
use crate::provider::Socket;
use crate::sql::Sql;
use crate::sync::{RemovedTransportData, SyncData, TransportData};
@@ -69,9 +68,7 @@ impl TryFrom<Socket> for ConnectionSecurity {
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub(crate) enum ConfiguredCertificateChecks {
/// Use configuration from the provider database.
/// If there is no provider database setting for certificate checks,
/// accept invalid certificates.
/// Accept invalid certificates.
///
/// Must not be saved by new versions.
///
@@ -95,9 +92,8 @@ pub(crate) enum ConfiguredCertificateChecks {
/// Alias to `AcceptInvalidCertificates` for compatibility.
AcceptInvalidCertificates2 = 3,
/// Use configuration from the provider database.
/// If there is no provider database setting for certificate checks,
/// apply strict checks to TLS certificates.
/// Apply strict checks to TLS certificates,
/// unless a legacy-domain override disables them.
Automatic = 4,
}
@@ -170,8 +166,7 @@ pub(crate) struct ConfiguredLoginParam {
/// Custom IMAP user.
///
/// This overwrites autoconfig from the provider database
/// if non-empty.
/// This overwrites autoconfig if non-empty.
pub imap_user: String,
pub imap_password: String,
@@ -187,14 +182,11 @@ pub(crate) struct ConfiguredLoginParam {
/// Custom SMTP user.
///
/// This overwrites autoconfig from the provider database
/// if non-empty.
/// This overwrites autoconfig if non-empty.
pub smtp_user: String,
pub smtp_password: String,
pub provider: Option<&'static Provider>,
/// TLS options: whether to allow invalid certificates and/or
/// invalid hostnames
pub certificate_checks: ConfiguredCertificateChecks,
@@ -218,16 +210,12 @@ pub(crate) struct ConfiguredLoginParamJson {
pub smtp: Vec<ConfiguredServerLoginParam>,
pub smtp_user: String,
pub smtp_password: String,
pub provider_id: Option<String>,
pub certificate_checks: ConfiguredCertificateChecks,
}
impl fmt::Display for ConfiguredLoginParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let provider_id = match self.provider {
Some(provider) => provider.id,
None => "none",
};
let certificate_checks = self.certificate_checks;
if let Ok(parsed_addr) = EmailAddress::new(&self.addr) {
// Only include the domain.
@@ -260,7 +248,7 @@ impl fmt::Display for ConfiguredLoginParam {
write!(f, "{smtp}")?;
first = false;
}
write!(f, "] provider:{provider_id} cert_{certificate_checks}")?;
write!(f, "] cert_{certificate_checks}")?;
Ok(())
}
}
@@ -344,12 +332,6 @@ impl ConfiguredLoginParam {
.get_config(Config::ConfiguredMailPw)
.await?
.context("IMAP password is not configured")?;
let provider = context
.get_config(Config::ConfiguredProvider)
.await?
.and_then(|cfg| get_provider_by_id(&cfg));
let imap;
let smtp;
@@ -362,149 +344,7 @@ impl ConfiguredLoginParam {
.await?
.unwrap_or_default();
if let Some(provider) = provider {
let parsed_addr = EmailAddress::new(&addr).context("Bad email-address")?;
let addr_localpart = parsed_addr.local;
if provider.server.is_empty() {
let servers = vec![
ServerParams {
protocol: Protocol::Imap,
hostname: context
.get_config(Config::ConfiguredMailServer)
.await?
.unwrap_or_default(),
port: context
.get_config_parsed::<u16>(Config::ConfiguredMailPort)
.await?
.unwrap_or_default(),
socket: context
.get_config_parsed::<i32>(Config::ConfiguredMailSecurity)
.await?
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default(),
username: mail_user.clone(),
},
ServerParams {
protocol: Protocol::Smtp,
hostname: context
.get_config(Config::ConfiguredSendServer)
.await?
.unwrap_or_default(),
port: context
.get_config_parsed::<u16>(Config::ConfiguredSendPort)
.await?
.unwrap_or_default(),
socket: context
.get_config_parsed::<i32>(Config::ConfiguredSendSecurity)
.await?
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default(),
username: send_user.clone(),
},
];
let servers = expand_param_vector(servers, &addr, &parsed_addr.domain);
imap = servers
.iter()
.filter_map(|params| {
let Ok(security) = params.socket.try_into() else {
return None;
};
if params.protocol == Protocol::Imap {
Some(ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: params.hostname.clone(),
port: params.port,
security,
},
user: params.username.clone(),
})
} else {
None
}
})
.collect();
smtp = servers
.iter()
.filter_map(|params| {
let Ok(security) = params.socket.try_into() else {
return None;
};
if params.protocol == Protocol::Smtp {
Some(ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: params.hostname.clone(),
port: params.port,
security,
},
user: params.username.clone(),
})
} else {
None
}
})
.collect();
} else {
imap = provider
.server
.iter()
.filter_map(|server| {
if server.protocol != Protocol::Imap {
return None;
}
let Ok(security) = server.socket.try_into() else {
return None;
};
Some(ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: server.hostname.to_string(),
port: server.port,
security,
},
user: if !mail_user.is_empty() {
mail_user.clone()
} else {
match server.username_pattern {
UsernamePattern::Email => addr.to_string(),
UsernamePattern::Emaillocalpart => addr_localpart.clone(),
}
},
})
})
.collect();
smtp = provider
.server
.iter()
.filter_map(|server| {
if server.protocol != Protocol::Smtp {
return None;
}
let Ok(security) = server.socket.try_into() else {
return None;
};
Some(ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: server.hostname.to_string(),
port: server.port,
security,
},
user: if !send_user.is_empty() {
send_user.clone()
} else {
match server.username_pattern {
UsernamePattern::Email => addr.to_string(),
UsernamePattern::Emaillocalpart => addr_localpart.clone(),
}
},
})
})
.collect();
}
} else if let (Some(configured_mail_servers), Some(configured_send_servers)) = (
if let (Some(configured_mail_servers), Some(configured_send_servers)) = (
context.get_config(Config::ConfiguredImapServers).await?,
context.get_config(Config::ConfiguredSmtpServers).await?,
) {
@@ -571,7 +411,6 @@ impl ConfiguredLoginParam {
smtp_user: send_user,
smtp_password: send_pw,
certificate_checks,
provider,
}))
}
@@ -602,7 +441,6 @@ impl ConfiguredLoginParam {
.is_none_or(|folder| !folder.is_empty()),
"Configured watched folder name cannot be empty"
);
let provider = json.provider_id.and_then(|id| get_provider_by_id(&id));
Ok(ConfiguredLoginParam {
addr: json.addr,
@@ -613,7 +451,7 @@ impl ConfiguredLoginParam {
smtp: json.smtp,
smtp_user: json.smtp_user,
smtp_password: json.smtp_password,
provider,
certificate_checks: json.certificate_checks,
})
}
@@ -624,12 +462,12 @@ impl ConfiguredLoginParam {
}
pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
let disable_strict_tls =
crate::provider::legacy_settings_for_addr(&self.addr).disable_strict_tls;
match self.certificate_checks {
ConfiguredCertificateChecks::OldAutomatic => {
provider_strict_tls.unwrap_or(connected_through_proxy)
}
ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true),
ConfiguredCertificateChecks::OldAutomatic if disable_strict_tls => false,
ConfiguredCertificateChecks::OldAutomatic => connected_through_proxy,
ConfiguredCertificateChecks::Automatic => !disable_strict_tls,
ConfiguredCertificateChecks::Strict => true,
ConfiguredCertificateChecks::AcceptInvalidCertificates
| ConfiguredCertificateChecks::AcceptInvalidCertificates2 => false,
@@ -648,7 +486,7 @@ impl From<ConfiguredLoginParam> for ConfiguredLoginParamJson {
smtp: configured_login_param.smtp,
smtp_user: configured_login_param.smtp_user,
smtp_password: configured_login_param.smtp_password,
provider_id: configured_login_param.provider.map(|p| p.id.to_string()),
certificate_checks: configured_login_param.certificate_checks,
}
}

View File

@@ -4,8 +4,6 @@ use std::time::Duration;
use crate::tools::SystemTime;
use super::*;
use crate::log::LogExt as _;
use crate::provider::get_provider_by_id;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools::time;
@@ -47,7 +45,6 @@ async fn test_save_load_login_param() -> Result<()> {
}],
smtp_user: "".to_string(),
smtp_password: "bar".to_string(),
provider: None,
certificate_checks: ConfiguredCertificateChecks::Strict,
};
@@ -55,7 +52,7 @@ async fn test_save_load_login_param() -> Result<()> {
.clone()
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
.await?;
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_folder":"Folder","imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict"}"#;
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_folder":"Folder","imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","certificate_checks":"Strict"}"#;
assert_eq!(
t.sql
.query_get_value::<String>("SELECT configured_param FROM transports", ())
@@ -72,7 +69,6 @@ async fn test_save_load_login_param() -> Result<()> {
assert!(formatted.contains(" imap:[imap.example.com:123:starttls]"));
assert!(formatted.contains(" folder:\"Folder\""));
assert!(formatted.contains(" smtp:[smtp.example.com:456:tls]"));
assert!(formatted.contains(" provider:none"));
assert!(formatted.contains(" cert_strict"));
// Legacy ConfiguredImapCertificateChecks config is ignored
@@ -91,176 +87,7 @@ async fn test_save_load_login_param() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_posteo_alias() -> Result<()> {
let t = TestContext::new().await;
let user = "alice@posteo.de";
// Alice has old config with "alice@posteo.at" address
// and "alice@posteo.de" username.
t.set_config(Config::Configured, Some("1")).await?;
t.set_config(Config::ConfiguredProvider, Some("posteo"))
.await?;
t.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at"))
.await?;
t.set_config(Config::ConfiguredMailServer, Some("posteo.de"))
.await?;
t.set_config(Config::ConfiguredMailPort, Some("993"))
.await?;
t.set_config(Config::ConfiguredMailSecurity, Some("1"))
.await?; // TLS
t.set_config(Config::ConfiguredMailUser, Some(user)).await?;
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredSendServer, Some("posteo.de"))
.await?;
t.set_config(Config::ConfiguredSendPort, Some("465"))
.await?;
t.set_config(Config::ConfiguredSendSecurity, Some("1"))
.await?; // TLS
t.set_config(Config::ConfiguredSendUser, Some(user)).await?;
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
.await?;
let param = ConfiguredLoginParam {
addr: "alice@posteo.at".to_string(),
imap: vec![
ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: "posteo.de".to_string(),
port: 993,
security: ConnectionSecurity::Tls,
},
user: user.to_string(),
},
ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: "posteo.de".to_string(),
port: 143,
security: ConnectionSecurity::Starttls,
},
user: user.to_string(),
},
],
imap_folder: None,
imap_user: "alice@posteo.de".to_string(),
imap_password: "foobarbaz".to_string(),
smtp: vec![
ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: "posteo.de".to_string(),
port: 465,
security: ConnectionSecurity::Tls,
},
user: user.to_string(),
},
ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: "posteo.de".to_string(),
port: 587,
security: ConnectionSecurity::Starttls,
},
user: user.to_string(),
},
],
smtp_user: "alice@posteo.de".to_string(),
smtp_password: "foobarbaz".to_string(),
provider: get_provider_by_id("posteo"),
certificate_checks: ConfiguredCertificateChecks::Strict,
};
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
assert_eq!(loaded, param);
migrate_configured_login_param(&t).await;
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded, param);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_empty_server_list_legacy() -> Result<()> {
// Find a provider that does not have server list set.
//
// There is at least one such provider in the provider database.
let (domain, provider) = crate::provider::data::PROVIDER_DATA
.iter()
.find(|(_domain, provider)| provider.server.is_empty())
.unwrap();
let t = TestContext::new().await;
let addr = format!("alice@{domain}");
t.set_config(Config::Configured, Some("1")).await?;
t.set_config(Config::ConfiguredProvider, Some(provider.id))
.await?;
t.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
.await?;
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
migrate_configured_login_param(&t).await;
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
Ok(())
}
async fn migrate_configured_login_param(t: &TestContext) {
t.sql.execute("DROP TABLE transports;", ()).await.unwrap();
t.sql.set_raw_config_int("dbversion", 130).await.unwrap();
t.sql.run_migrations(t).await.log_err(t).ok();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_empty_server_list() -> Result<()> {
// Find a provider that does not have server list set.
//
// There is at least one such provider in the provider database.
let (domain, provider) = crate::provider::data::PROVIDER_DATA
.iter()
.find(|(_domain, provider)| provider.server.is_empty())
.unwrap();
let t = TestContext::new().await;
let addr = format!("alice@{domain}");
dummy_configured_login_param(&addr, Some(provider))
.save_to_transports_table(&t, &EnteredLoginParam::default(), time())
.await?;
let (_transport_id, loaded) = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
Ok(())
}
fn dummy_configured_login_param(
addr: &str,
provider: Option<&'static Provider>,
) -> ConfiguredLoginParam {
fn dummy_configured_login_param(addr: &str) -> ConfiguredLoginParam {
ConfiguredLoginParam {
addr: addr.to_string(),
imap: vec![ConfiguredServerLoginParam {
@@ -284,7 +111,6 @@ fn dummy_configured_login_param(
}],
smtp_user: addr.to_string(),
smtp_password: "foobarbaz".to_string(),
provider,
certificate_checks: ConfiguredCertificateChecks::Automatic,
}
}
@@ -312,7 +138,7 @@ async fn test_is_published_flag() -> Result<()> {
)
.await;
dummy_configured_login_param("alice@otherprovider.com", None)
dummy_configured_login_param("alice@otherprovider.com")
.save_to_transports_table(
alice,
&EnteredLoginParam {