mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 01:16:31 +03:00
feat: dc_accounts_set_push_device_token and dc_get_push_state APIs
This commit is contained in:
@@ -686,6 +686,24 @@ int dc_get_connectivity (dc_context_t* context);
|
|||||||
char* dc_get_connectivity_html (dc_context_t* context);
|
char* dc_get_connectivity_html (dc_context_t* context);
|
||||||
|
|
||||||
|
|
||||||
|
#define DC_PUSH_NOT_CONNECTED 0
|
||||||
|
#define DC_PUSH_HEARTBEAT 1
|
||||||
|
#define DC_PUSH_CONNECTED 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current push notification state.
|
||||||
|
* One of:
|
||||||
|
* - DC_PUSH_NOT_CONNECTED
|
||||||
|
* - DC_PUSH_HEARTBEAT
|
||||||
|
* - DC_PUSH_CONNECTED
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object.
|
||||||
|
* @return Push notification state.
|
||||||
|
*/
|
||||||
|
int dc_get_push_state (dc_context_t* context);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standalone version of dc_accounts_all_work_done().
|
* Standalone version of dc_accounts_all_work_done().
|
||||||
* Only used by the python tests.
|
* Only used by the python tests.
|
||||||
@@ -3165,6 +3183,16 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
|||||||
*/
|
*/
|
||||||
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
|
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets device token for Apple Push Notification service.
|
||||||
|
* Returns immediately.
|
||||||
|
*
|
||||||
|
* @memberof dc_accounts_t
|
||||||
|
* @param token Hexadecimal device token
|
||||||
|
*/
|
||||||
|
void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const char *token);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the event emitter that is used to receive events.
|
* Create the event emitter that is used to receive events.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
block_on(async move { ctx.get_connectivity().await as u32 as libc::c_int })
|
block_on(ctx.get_connectivity()) as u32 as libc::c_int
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -407,6 +407,16 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_get_push_state(context: *const dc_context_t) -> libc::c_int {
|
||||||
|
if context.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_get_push_state()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
block_on(ctx.push_state()) as libc::c_int
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
|
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
|
||||||
if context.is_null() {
|
if context.is_null() {
|
||||||
@@ -4919,6 +4929,29 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
|
|||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||||
|
accounts: *mut dc_accounts_t,
|
||||||
|
token: *const libc::c_char,
|
||||||
|
) {
|
||||||
|
if accounts.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_accounts_set_push_device_token()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let accounts = &*accounts;
|
||||||
|
let token = to_string_lossy(token);
|
||||||
|
|
||||||
|
block_on(async move {
|
||||||
|
let mut accounts = accounts.write().await;
|
||||||
|
if let Err(err) = accounts.set_push_device_token(&token).await {
|
||||||
|
accounts.emit_event(EventType::Error(format!(
|
||||||
|
"Failed to set notify token: {err:#}."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||||
accounts: *mut dc_accounts_t,
|
accounts: *mut dc_accounts_t,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use tokio::time::{sleep, Duration};
|
|||||||
|
|
||||||
use crate::context::{Context, ContextBuilder};
|
use crate::context::{Context, ContextBuilder};
|
||||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||||
|
use crate::push::PushSubscriber;
|
||||||
use crate::stock_str::StockStrings;
|
use crate::stock_str::StockStrings;
|
||||||
|
|
||||||
/// Account manager, that can handle multiple accounts in a single place.
|
/// Account manager, that can handle multiple accounts in a single place.
|
||||||
@@ -37,6 +38,9 @@ pub struct Accounts {
|
|||||||
/// This way changing a translation for one context automatically
|
/// This way changing a translation for one context automatically
|
||||||
/// changes it for all other contexts.
|
/// changes it for all other contexts.
|
||||||
pub(crate) stockstrings: StockStrings,
|
pub(crate) stockstrings: StockStrings,
|
||||||
|
|
||||||
|
/// Push notification subscriber shared between accounts.
|
||||||
|
push_subscriber: PushSubscriber,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Accounts {
|
impl Accounts {
|
||||||
@@ -73,8 +77,9 @@ impl Accounts {
|
|||||||
.context("failed to load accounts config")?;
|
.context("failed to load accounts config")?;
|
||||||
let events = Events::new();
|
let events = Events::new();
|
||||||
let stockstrings = StockStrings::new();
|
let stockstrings = StockStrings::new();
|
||||||
|
let push_subscriber = PushSubscriber::new();
|
||||||
let accounts = config
|
let accounts = config
|
||||||
.load_accounts(&events, &stockstrings, &dir)
|
.load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
|
||||||
.await
|
.await
|
||||||
.context("failed to load accounts")?;
|
.context("failed to load accounts")?;
|
||||||
|
|
||||||
@@ -84,6 +89,7 @@ impl Accounts {
|
|||||||
accounts,
|
accounts,
|
||||||
events,
|
events,
|
||||||
stockstrings,
|
stockstrings,
|
||||||
|
push_subscriber,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +130,7 @@ impl Accounts {
|
|||||||
.with_id(account_config.id)
|
.with_id(account_config.id)
|
||||||
.with_events(self.events.clone())
|
.with_events(self.events.clone())
|
||||||
.with_stock_strings(self.stockstrings.clone())
|
.with_stock_strings(self.stockstrings.clone())
|
||||||
|
.with_push_subscriber(self.push_subscriber.clone())
|
||||||
.build()
|
.build()
|
||||||
.await?;
|
.await?;
|
||||||
// Try to open without a passphrase,
|
// Try to open without a passphrase,
|
||||||
@@ -144,6 +151,7 @@ impl Accounts {
|
|||||||
.with_id(account_config.id)
|
.with_id(account_config.id)
|
||||||
.with_events(self.events.clone())
|
.with_events(self.events.clone())
|
||||||
.with_stock_strings(self.stockstrings.clone())
|
.with_stock_strings(self.stockstrings.clone())
|
||||||
|
.with_push_subscriber(self.push_subscriber.clone())
|
||||||
.build()
|
.build()
|
||||||
.await?;
|
.await?;
|
||||||
self.accounts.insert(account_config.id, ctx);
|
self.accounts.insert(account_config.id, ctx);
|
||||||
@@ -340,6 +348,12 @@ impl Accounts {
|
|||||||
pub fn get_event_emitter(&self) -> EventEmitter {
|
pub fn get_event_emitter(&self) -> EventEmitter {
|
||||||
self.events.get_emitter()
|
self.events.get_emitter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets notification token for Apple Push Notification service.
|
||||||
|
pub async fn set_push_device_token(&mut self, token: &str) -> Result<()> {
|
||||||
|
self.push_subscriber.set_device_token(token).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration file name.
|
/// Configuration file name.
|
||||||
@@ -525,6 +539,7 @@ impl Config {
|
|||||||
&self,
|
&self,
|
||||||
events: &Events,
|
events: &Events,
|
||||||
stockstrings: &StockStrings,
|
stockstrings: &StockStrings,
|
||||||
|
push_subscriber: PushSubscriber,
|
||||||
dir: &Path,
|
dir: &Path,
|
||||||
) -> Result<BTreeMap<u32, Context>> {
|
) -> Result<BTreeMap<u32, Context>> {
|
||||||
let mut accounts = BTreeMap::new();
|
let mut accounts = BTreeMap::new();
|
||||||
@@ -535,6 +550,7 @@ impl Config {
|
|||||||
.with_id(account_config.id)
|
.with_id(account_config.id)
|
||||||
.with_events(events.clone())
|
.with_events(events.clone())
|
||||||
.with_stock_strings(stockstrings.clone())
|
.with_stock_strings(stockstrings.clone())
|
||||||
|
.with_push_subscriber(push_subscriber.clone())
|
||||||
.build()
|
.build()
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
|
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ use crate::login_param::LoginParam;
|
|||||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||||
use crate::param::{Param, Params};
|
use crate::param::{Param, Params};
|
||||||
use crate::peerstate::Peerstate;
|
use crate::peerstate::Peerstate;
|
||||||
|
use crate::push::PushSubscriber;
|
||||||
use crate::quota::QuotaInfo;
|
use crate::quota::QuotaInfo;
|
||||||
use crate::scheduler::{convert_folder_meaning, SchedulerState};
|
use crate::scheduler::{convert_folder_meaning, SchedulerState};
|
||||||
use crate::sql::Sql;
|
use crate::sql::Sql;
|
||||||
@@ -86,6 +87,8 @@ pub struct ContextBuilder {
|
|||||||
events: Events,
|
events: Events,
|
||||||
stock_strings: StockStrings,
|
stock_strings: StockStrings,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
|
|
||||||
|
push_subscriber: Option<PushSubscriber>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextBuilder {
|
impl ContextBuilder {
|
||||||
@@ -101,6 +104,7 @@ impl ContextBuilder {
|
|||||||
events: Events::new(),
|
events: Events::new(),
|
||||||
stock_strings: StockStrings::new(),
|
stock_strings: StockStrings::new(),
|
||||||
password: None,
|
password: None,
|
||||||
|
push_subscriber: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,10 +159,23 @@ impl ContextBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets push subscriber.
|
||||||
|
pub(crate) fn with_push_subscriber(mut self, push_subscriber: PushSubscriber) -> Self {
|
||||||
|
self.push_subscriber = Some(push_subscriber);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds the [`Context`] without opening it.
|
/// Builds the [`Context`] without opening it.
|
||||||
pub async fn build(self) -> Result<Context> {
|
pub async fn build(self) -> Result<Context> {
|
||||||
let context =
|
let push_subscriber = self.push_subscriber.unwrap_or_default();
|
||||||
Context::new_closed(&self.dbfile, self.id, self.events, self.stock_strings).await?;
|
let context = Context::new_closed(
|
||||||
|
&self.dbfile,
|
||||||
|
self.id,
|
||||||
|
self.events,
|
||||||
|
self.stock_strings,
|
||||||
|
push_subscriber,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(context)
|
Ok(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +280,13 @@ pub struct InnerContext {
|
|||||||
/// Standard RwLock instead of [`tokio::sync::RwLock`] is used
|
/// Standard RwLock instead of [`tokio::sync::RwLock`] is used
|
||||||
/// because the lock is used from synchronous [`Context::emit_event`].
|
/// because the lock is used from synchronous [`Context::emit_event`].
|
||||||
pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,
|
pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,
|
||||||
|
|
||||||
|
/// Push subscriber to store device token
|
||||||
|
/// and register for heartbeat notifications.
|
||||||
|
pub(crate) push_subscriber: PushSubscriber,
|
||||||
|
|
||||||
|
/// True if account has subscribed to push notifications via IMAP.
|
||||||
|
pub(crate) push_subscribed: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The state of ongoing process.
|
/// The state of ongoing process.
|
||||||
@@ -308,7 +332,8 @@ impl Context {
|
|||||||
events: Events,
|
events: Events,
|
||||||
stock_strings: StockStrings,
|
stock_strings: StockStrings,
|
||||||
) -> Result<Context> {
|
) -> Result<Context> {
|
||||||
let context = Self::new_closed(dbfile, id, events, stock_strings).await?;
|
let context =
|
||||||
|
Self::new_closed(dbfile, id, events, stock_strings, Default::default()).await?;
|
||||||
|
|
||||||
// Open the database if is not encrypted.
|
// Open the database if is not encrypted.
|
||||||
if context.check_passphrase("".to_string()).await? {
|
if context.check_passphrase("".to_string()).await? {
|
||||||
@@ -323,6 +348,7 @@ impl Context {
|
|||||||
id: u32,
|
id: u32,
|
||||||
events: Events,
|
events: Events,
|
||||||
stockstrings: StockStrings,
|
stockstrings: StockStrings,
|
||||||
|
push_subscriber: PushSubscriber,
|
||||||
) -> Result<Context> {
|
) -> Result<Context> {
|
||||||
let mut blob_fname = OsString::new();
|
let mut blob_fname = OsString::new();
|
||||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||||
@@ -331,7 +357,14 @@ impl Context {
|
|||||||
if !blobdir.exists() {
|
if !blobdir.exists() {
|
||||||
tokio::fs::create_dir_all(&blobdir).await?;
|
tokio::fs::create_dir_all(&blobdir).await?;
|
||||||
}
|
}
|
||||||
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events, stockstrings)?;
|
let context = Context::with_blobdir(
|
||||||
|
dbfile.into(),
|
||||||
|
blobdir,
|
||||||
|
id,
|
||||||
|
events,
|
||||||
|
stockstrings,
|
||||||
|
push_subscriber,
|
||||||
|
)?;
|
||||||
Ok(context)
|
Ok(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,6 +407,7 @@ impl Context {
|
|||||||
id: u32,
|
id: u32,
|
||||||
events: Events,
|
events: Events,
|
||||||
stockstrings: StockStrings,
|
stockstrings: StockStrings,
|
||||||
|
push_subscriber: PushSubscriber,
|
||||||
) -> Result<Context> {
|
) -> Result<Context> {
|
||||||
ensure!(
|
ensure!(
|
||||||
blobdir.is_dir(),
|
blobdir.is_dir(),
|
||||||
@@ -408,6 +442,8 @@ impl Context {
|
|||||||
last_full_folder_scan: Mutex::new(None),
|
last_full_folder_scan: Mutex::new(None),
|
||||||
last_error: std::sync::RwLock::new("".to_string()),
|
last_error: std::sync::RwLock::new("".to_string()),
|
||||||
debug_logging: std::sync::RwLock::new(None),
|
debug_logging: std::sync::RwLock::new(None),
|
||||||
|
push_subscriber,
|
||||||
|
push_subscribed: AtomicBool::new(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
let ctx = Context {
|
let ctx = Context {
|
||||||
@@ -1509,7 +1545,14 @@ mod tests {
|
|||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
let blobdir = PathBuf::new();
|
let blobdir = PathBuf::new();
|
||||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
|
let res = Context::with_blobdir(
|
||||||
|
dbfile,
|
||||||
|
blobdir,
|
||||||
|
1,
|
||||||
|
Events::new(),
|
||||||
|
StockStrings::new(),
|
||||||
|
Default::default(),
|
||||||
|
);
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1518,7 +1561,14 @@ mod tests {
|
|||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let dbfile = tmp.path().join("db.sqlite");
|
let dbfile = tmp.path().join("db.sqlite");
|
||||||
let blobdir = tmp.path().join("blobs");
|
let blobdir = tmp.path().join("blobs");
|
||||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
|
let res = Context::with_blobdir(
|
||||||
|
dbfile,
|
||||||
|
blobdir,
|
||||||
|
1,
|
||||||
|
Events::new(),
|
||||||
|
StockStrings::new(),
|
||||||
|
Default::default(),
|
||||||
|
);
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1741,16 +1791,18 @@ mod tests {
|
|||||||
let dir = tempdir()?;
|
let dir = tempdir()?;
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
let dbfile = dir.path().join("db.sqlite");
|
||||||
|
|
||||||
let id = 1;
|
let context = ContextBuilder::new(dbfile.clone())
|
||||||
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
|
.with_id(1)
|
||||||
|
.build()
|
||||||
.await
|
.await
|
||||||
.context("failed to create context")?;
|
.context("failed to create context")?;
|
||||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||||
assert_eq!(context.is_open().await, true);
|
assert_eq!(context.is_open().await, true);
|
||||||
drop(context);
|
drop(context);
|
||||||
|
|
||||||
let id = 2;
|
let context = ContextBuilder::new(dbfile)
|
||||||
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
|
.with_id(2)
|
||||||
|
.build()
|
||||||
.await
|
.await
|
||||||
.context("failed to create context")?;
|
.context("failed to create context")?;
|
||||||
assert_eq!(context.is_open().await, false);
|
assert_eq!(context.is_open().await, false);
|
||||||
@@ -1766,8 +1818,9 @@ mod tests {
|
|||||||
let dir = tempdir()?;
|
let dir = tempdir()?;
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
let dbfile = dir.path().join("db.sqlite");
|
||||||
|
|
||||||
let id = 1;
|
let context = ContextBuilder::new(dbfile)
|
||||||
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
|
.with_id(1)
|
||||||
|
.build()
|
||||||
.await
|
.await
|
||||||
.context("failed to create context")?;
|
.context("failed to create context")?;
|
||||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||||
|
|||||||
32
src/imap.rs
32
src/imap.rs
@@ -8,6 +8,7 @@ use std::{
|
|||||||
collections::{BTreeMap, BTreeSet, HashMap},
|
collections::{BTreeMap, BTreeSet, HashMap},
|
||||||
iter::Peekable,
|
iter::Peekable,
|
||||||
mem::take,
|
mem::take,
|
||||||
|
sync::atomic::Ordering,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1445,6 +1446,37 @@ impl Session {
|
|||||||
*lock = Some(ServerMetadata { comment, admin });
|
*lock = Some(ServerMetadata { comment, admin });
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stores device token into /private/devicetoken IMAP METADATA of the Inbox.
|
||||||
|
pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
|
||||||
|
if context.push_subscribed.load(Ordering::Relaxed) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(device_token) = context.push_subscriber.device_token().await else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.can_metadata() && self.can_push() {
|
||||||
|
let folder = context
|
||||||
|
.get_config(Config::ConfiguredInboxFolder)
|
||||||
|
.await?
|
||||||
|
.context("INBOX is not configured")?;
|
||||||
|
|
||||||
|
self.run_command_and_check_ok(format!(
|
||||||
|
"SETMETADATA \"{folder}\" (/private/devicetoken \"{device_token}\")"
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.context("SETMETADATA command failed")?;
|
||||||
|
context.push_subscribed.store(true, Ordering::Relaxed);
|
||||||
|
} else if !context.push_subscriber.heartbeat_subscribed().await {
|
||||||
|
let context = context.clone();
|
||||||
|
// Subscribe for heartbeat notifications.
|
||||||
|
tokio::spawn(async move { context.push_subscriber.subscribe().await });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ pub(crate) struct Capabilities {
|
|||||||
/// <https://tools.ietf.org/html/rfc5464>
|
/// <https://tools.ietf.org/html/rfc5464>
|
||||||
pub can_metadata: bool,
|
pub can_metadata: bool,
|
||||||
|
|
||||||
|
/// True if the server supports XDELTAPUSH capability.
|
||||||
|
/// This capability means setting /private/devicetoken IMAP METADATA
|
||||||
|
/// on the INBOX results in new mail notifications
|
||||||
|
/// via notifications.delta.chat service.
|
||||||
|
/// This is supported by <https://github.com/deltachat/chatmail>
|
||||||
|
pub can_push: bool,
|
||||||
|
|
||||||
/// Server ID if the server supports ID capability.
|
/// Server ID if the server supports ID capability.
|
||||||
pub server_id: Option<HashMap<String, String>>,
|
pub server_id: Option<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ async fn determine_capabilities(
|
|||||||
can_check_quota: caps.has_str("QUOTA"),
|
can_check_quota: caps.has_str("QUOTA"),
|
||||||
can_condstore: caps.has_str("CONDSTORE"),
|
can_condstore: caps.has_str("CONDSTORE"),
|
||||||
can_metadata: caps.has_str("METADATA"),
|
can_metadata: caps.has_str("METADATA"),
|
||||||
|
can_push: caps.has_str("XDELTAPUSH"),
|
||||||
server_id,
|
server_id,
|
||||||
};
|
};
|
||||||
Ok(capabilities)
|
Ok(capabilities)
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ impl Session {
|
|||||||
self.capabilities.can_metadata
|
self.capabilities.can_metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn can_push(&self) -> bool {
|
||||||
|
self.capabilities.can_push
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the names of all folders on the IMAP server.
|
/// Returns the names of all folders on the IMAP server.
|
||||||
pub async fn list_folders(&mut self) -> Result<Vec<async_imap::types::Name>> {
|
pub async fn list_folders(&mut self) -> Result<Vec<async_imap::types::Name>> {
|
||||||
let list = self.list(Some(""), Some("*")).await?.try_collect().await?;
|
let list = self.list(Some(""), Some("*")).await?.try_collect().await?;
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ mod color;
|
|||||||
pub mod html;
|
pub mod html;
|
||||||
pub mod net;
|
pub mod net;
|
||||||
pub mod plaintext;
|
pub mod plaintext;
|
||||||
|
mod push;
|
||||||
pub mod summary;
|
pub mod summary;
|
||||||
|
|
||||||
mod debug_logging;
|
mod debug_logging;
|
||||||
|
|||||||
115
src/push.rs
Normal file
115
src/push.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::net::http;
|
||||||
|
|
||||||
|
/// Manages subscription to Apple Push Notification services.
|
||||||
|
///
|
||||||
|
/// This structure is created by account manager and is shared between accounts.
|
||||||
|
/// To enable notifications, application should request the device token as described in
|
||||||
|
/// <https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns>
|
||||||
|
/// and give it to the account manager, which will forward the token in this structure.
|
||||||
|
///
|
||||||
|
/// Each account (context) can then retrieve device token
|
||||||
|
/// from this structure and give it to the email server.
|
||||||
|
/// If email server does not support push notifications,
|
||||||
|
/// account can call `subscribe` method
|
||||||
|
/// to register device token with the heartbeat
|
||||||
|
/// notification provider server as a fallback.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct PushSubscriber {
|
||||||
|
inner: Arc<RwLock<PushSubscriberState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PushSubscriber {
|
||||||
|
/// Creates new push notification subscriber.
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets device token for Apple Push Notification service.
|
||||||
|
pub(crate) async fn set_device_token(&mut self, token: &str) {
|
||||||
|
self.inner.write().await.device_token = Some(token.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves device token.
|
||||||
|
///
|
||||||
|
/// Token may be not available if application is not running on Apple platform,
|
||||||
|
/// failed to register for remote notifications or is in the process of registering.
|
||||||
|
///
|
||||||
|
/// IMAP loop should periodically check if device token is available
|
||||||
|
/// and send the token to the email server if it supports push notifications.
|
||||||
|
pub(crate) async fn device_token(&self) -> Option<String> {
|
||||||
|
self.inner.read().await.device_token.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes for heartbeat notifications with previously set device token.
|
||||||
|
pub(crate) async fn subscribe(&self) -> Result<()> {
|
||||||
|
let mut state = self.inner.write().await;
|
||||||
|
|
||||||
|
if state.heartbeat_subscribed {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(ref token) = state.device_token else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let socks5_config = None;
|
||||||
|
let response = http::get_client(socks5_config)?
|
||||||
|
.post("https://notifications.delta.chat/register")
|
||||||
|
.body(format!("{{\"token\":\"{token}\"}}"))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response_status = response.status();
|
||||||
|
if response_status.is_success() {
|
||||||
|
state.heartbeat_subscribed = true;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn heartbeat_subscribed(&self) -> bool {
|
||||||
|
self.inner.read().await.heartbeat_subscribed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct PushSubscriberState {
|
||||||
|
/// Device token.
|
||||||
|
device_token: Option<String>,
|
||||||
|
|
||||||
|
/// If subscribed to heartbeat push notifications.
|
||||||
|
heartbeat_subscribed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||||
|
#[repr(i8)]
|
||||||
|
pub enum NotifyState {
|
||||||
|
/// Not subscribed to push notifications.
|
||||||
|
#[default]
|
||||||
|
NotConnected = 0,
|
||||||
|
|
||||||
|
/// Subscribed to heartbeat push notifications.
|
||||||
|
Heartbeat = 1,
|
||||||
|
|
||||||
|
/// Subscribed to push notifications for new messages.
|
||||||
|
Connected = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
/// Returns push notification subscriber state.
|
||||||
|
pub async fn push_state(&self) -> NotifyState {
|
||||||
|
if self.push_subscribed.load(Ordering::Relaxed) {
|
||||||
|
NotifyState::Connected
|
||||||
|
} else if self.push_subscriber.heartbeat_subscribed().await {
|
||||||
|
NotifyState::Heartbeat
|
||||||
|
} else {
|
||||||
|
NotifyState::NotConnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -525,6 +525,10 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
|||||||
.fetch_metadata(ctx)
|
.fetch_metadata(ctx)
|
||||||
.await
|
.await
|
||||||
.context("Failed to fetch metadata")?;
|
.context("Failed to fetch metadata")?;
|
||||||
|
session
|
||||||
|
.register_token(ctx)
|
||||||
|
.await
|
||||||
|
.context("Failed to register push token")?;
|
||||||
|
|
||||||
let session = fetch_idle(ctx, imap, session, FolderMeaning::Inbox).await?;
|
let session = fetch_idle(ctx, imap, session, FolderMeaning::Inbox).await?;
|
||||||
Ok(session)
|
Ok(session)
|
||||||
|
|||||||
Reference in New Issue
Block a user