im-core 0.1.0

Rust IM SDK for Awiki clients built on Agent Network Protocol (ANP)
Documentation
use im_core::prelude::*;
use rusqlite::Connection;
use serde_json::json;
use std::fs;
use tempfile::tempdir;

#[test]
fn email_notifications_are_owner_scoped_and_legacy_compatible() {
    let temp = tempdir().unwrap();
    let db_path = temp.path().join("state.sqlite3");
    let db = Connection::open(&db_path).unwrap();
    im_core::compat::local_state::ensure_schema(&db).unwrap();
    db.execute(
        r#"
INSERT INTO messages
    (msg_id, owner_identity_id, owner_did, thread_id, direction, content_type, title, content, stored_at, metadata)
VALUES (?1, ?2, ?3, ?4, 0, ?5, ?6, ?7, ?8, ?9)"#,
        (
            "mail-1",
            "alice-id",
            "did:example:alice",
            "mail:alice@example.com",
            "mail.notification",
            "[邮件] ",
            "legacy content",
            "2026-05-21T00:00:00Z",
            r#"{"mailbox_address":"alice@example.com","subject":"","from_addr":"sender@example.com","preview":"Preview","has_attachments":"yes"}"#,
        ),
    )
    .unwrap();
    db.execute(
        r#"
INSERT INTO messages
    (msg_id, owner_identity_id, owner_did, thread_id, direction, content_type, title, content, stored_at, metadata)
VALUES (?1, ?2, ?3, ?4, 0, ?5, ?6, ?7, ?8, ?9)"#,
        (
            "mail-other",
            "other-id",
            "did:example:other",
            "mail:other@example.com",
            "mail.notification",
            "Other",
            "other content",
            "2026-05-22T00:00:00Z",
            r#"{"source_kind":"mail"}"#,
        ),
    )
    .unwrap();

    let page = im_core::compat::local_state::list_email_notifications_for_test(
        &db_path,
        "alice-id",
        "did:example:alice",
        PageLimit::new(20).unwrap(),
    )
    .expect("notifications");

    assert_eq!(page.items.len(), 1);
    let item = &page.items[0];
    assert_eq!(item.id.as_str(), "mail-1");
    assert_eq!(
        item.mailbox_address.as_ref().map(EmailAddress::as_str),
        Some("alice@example.com")
    );
    assert_eq!(item.subject, "(no subject)");
    assert_eq!(item.from_addr.as_deref(), Some("sender@example.com"));
    assert_eq!(item.preview.as_deref(), Some("Preview"));
    assert!(item.has_attachments);
}

#[tokio::test]
async fn email_notifications_async_are_actor_backed_and_owner_scoped() {
    let temp = tempdir().unwrap();
    write_identity_fixture(temp.path());
    let core = ImCore::new(
        ImCoreConfig {
            service_base_url: ServiceEndpoint::parse("https://example.test").unwrap(),
            did_domain: "awiki.test".to_string(),
            user_service_endpoint: None,
            message_service_endpoint: None,
            mail_service_endpoint: None,
            anp_service_endpoint: None,
            anp_service_did: None,
            ca_bundle: None,
            transport_policy: MessageTransportPolicy::HttpOnly,
        },
        ImCorePaths {
            identities: IdentityRegistryPaths {
                identity_root_dir: temp.path().join("identities"),
                registry_path: temp.path().join("identities").join("registry.json"),
                default_identity_path: Some(temp.path().join("identities").join("default")),
            },
            local_state: LocalStatePaths {
                sqlite_path: temp.path().join("local").join("im.sqlite"),
            },
            runtime: RuntimePaths {
                cache_dir: temp.path().join("cache"),
                temp_dir: temp.path().join("tmp"),
            },
        },
    )
    .unwrap();
    let client = core.client_async(IdentitySelector::Default).await.unwrap();

    let db = Connection::open(temp.path().join("local").join("im.sqlite")).unwrap();
    im_core::compat::local_state::ensure_schema(&db).unwrap();
    db.execute(
        r#"
INSERT INTO messages
    (msg_id, owner_identity_id, owner_did, thread_id, direction, content_type, title, content, stored_at, metadata)
VALUES (?1, ?2, ?3, ?4, 0, ?5, ?6, ?7, ?8, ?9)"#,
        (
            "mail-async",
            "alice-id",
            "did:example:alice",
            "mail:alice@example.com",
            "mail.notification",
            "[邮件] Async subject",
            "legacy content",
            "2026-05-21T00:00:00Z",
            r#"{"mailbox_address":"alice@example.com","from_addr":"sender@example.com","preview":"Async preview","has_attachments":true}"#,
        ),
    )
    .unwrap();
    drop(db);

    let page = client
        .email()
        .notifications_async(EmailNotificationQuery {
            limit: PageLimit::new(20).unwrap(),
        })
        .await
        .unwrap();

    assert_eq!(page.items.len(), 1);
    let item = &page.items[0];
    assert_eq!(item.id.as_str(), "mail-async");
    assert_eq!(
        item.mailbox_address.as_ref().map(EmailAddress::as_str),
        Some("alice@example.com")
    );
    assert_eq!(item.subject, "Async subject");
    assert_eq!(item.from_addr.as_deref(), Some("sender@example.com"));
    assert_eq!(item.preview.as_deref(), Some("Async preview"));
    assert!(item.has_attachments);
}

fn write_identity_fixture(root: &std::path::Path) {
    let identity_root = root.join("identities");
    let identity_dir = identity_root.join("alice");
    fs::create_dir_all(&identity_dir).unwrap();
    fs::create_dir_all(root.join("local")).unwrap();
    fs::write(identity_root.join("default"), "alice\n").unwrap();
    fs::write(
        identity_root.join("registry.json"),
        json!({
            "default_identity": "alice",
            "identities": [{
                "id": "alice-id",
                "did": "did:example:alice",
                "local_alias": "alice",
                "handle": "alice.awiki.test",
                "ready_for_auth": true,
                "ready_for_messaging": true,
                "missing": []
            }]
        })
        .to_string(),
    )
    .unwrap();
    fs::write(
        identity_dir.join("did.json"),
        r#"{"id":"did:example:alice","controller":"did:example:alice"}"#,
    )
    .unwrap();
    fs::write(identity_dir.join("private.key"), "key\n").unwrap();
    fs::write(
        identity_dir.join("auth.json"),
        r#"{"jwt_token":"test-token"}"#,
    )
    .unwrap();
}