Ezt dobta a gép

Programozzon helyettünk a ChatGPT

Ezt a képet a Midjourney generálta nekünk

Egy ideje már motoszkált a fejemben egy ötlet, hogy hogyan lehetne az email fiókokat egy kicsit félrehasználni. Az alapja az, hogy az IMAP protokolon keresztül tetszőleges email-szerű dolgokat hozhatunk létre a saját fiókunkban, anélkül, hogy ténylegesen levelet küldtünk volna.

Például egy oldalon rányomunk egy "Elteszem későbbre" gombra, mire az email fiókunkban megjelenik egy olvasatlan email az oldal tartalmával. Vagy feliratkozunk oldalak RSS feed-jére és az új cikkek email-ként jelennek meg nálunk. Szóval amolyan adatbázis-féleségnek lehetne használni, amihez már létezik egy rakás kliens az elképzelhető összes platformra.

Egy kis extra intelligencia

Telt-múlt az idő, de a projektből csak nem lett semmi, míg egy napon a ChatGPT-vel (GPT-4 modell, de én csak Dave-nek hívtam) beszélgettem arról, hogy milyen szórakoztató hétvégi projekteket tudna ajánlani. Nem mozgatta meg a fantáziámat a válasza, de eszembe jutott, hogy van már nekem egy hétvégi projektem, amit meg kellene csinálni.

Rá is kérdeztem nála, hogy hogyan lehet Python-ban RSS feed-eket feldolgozni. Aztán arra, hogy hogyan lehet email üzeneteket létrehozni és elmenteni őket IMAP protokolon keresztül. A válaszok első ránézésre egész meggyőzőek voltak, úgyhogy megírattam vele a teljes projektet: csináljon email üzenetet egy RSS feed minden eleméből, amit aztán IMAP segítségével mentsen el egy email fiókba, mindezt természetesen Python nyelven.

Ezen a ponton átéltem egy kisebb egzisztenciális válságot, hogy mi szükség van még rám, csak kérdezzétek meg a ChatGPT-t, neki sokkal kevesebb ideig tartana megírni ezt a cikket is, én meg itt szerencsétlenkedek már órák óta.

De félre a borúlátással, végül úgy döntöttem, hogy írja újra az egészet Rust-ban és én majd megpróbálom lefuttatni. Az úgyis trendi dolog és még csak nem is értek hozzá, úgyhogy izgalmasabb is lesz.

Indulhat a projekt

Először is kaptam egy listát, hogy ezekre a dependenciákra lesz szükségem a Cargo.toml fájlomban:

[dependencies]
rss = "1.10.0"
lettre = "0.10.0-rc.3"
imap = "3.0.0"
native-tls = "0.2.8"
tokio = { version = "1.0", features = ["full"] }

Nagyjából stimmel, de az imap-ból nincs (még) 3.0.0 verzió, úgyhogy azt átírtam 3.0.0-alpha.10-re, mert az volt épp a legfrissebb. Ha rajtam múlt volna, akkor inkább a legfrissebb stabil verziót használom mindenből, de nem azért fizetnek, hogy gondolkozzak (nem is fizetnek).

Következőnek jött az RSS feed letöltése és feldolgozása:

use rss::Channel;

async fn parse_rss_feed(url: &str) -> Result<Channel, Box<dyn std::error::Error>> {
    let content = reqwest::get(url).await?.bytes().await?;
    let channel = Channel::read_from(&content[..])?;
    Ok(channel)
}

Itt panaszkodott a fordító, hogy ő ugyan nem tud semmit a reqwest modulról, úgyhogy ki kellett még egészítenem a Cargo.toml-t egy reqwest = "0.11.16" sorral.

A következő kódrészlet az email üzenet létrehozása az RSS feed elemből volt:

use lettre::message::{Header, Message, Mailbox};

fn create_email_from_entry(entry: &rss::Item, sender: &str, recipient: &str) -> Message {
    let body = format!("{}\n\nRead more at: {}", entry.description().unwrap_or_default(), entry.link().unwrap_or_default());
    Message::builder()
        .from(sender.parse().unwrap())
        .to(recipient.parse().unwrap())
        .subject(entry.title().unwrap_or_default())
        .header(Header::new("Content-Type", "text/plain; charset=utf-8"))
        .body(body)
        .unwrap()
}

A Header használata nem tetszett a fordítónak, mivel az szerinte privát. A Mailbox pedig feleslegesen van behúzva. Szerencsére text/plain tartalomra van beépített támogatás, úgyhogy végül erre az eredményre jutottam:

use lettre::message::{header::ContentType, Message};

fn create_email_from_entry(entry: &rss::Item, sender: &str, recipient: &str) -> Message {
    let body = format!("{}\n\nRead more at: {}", entry.description().unwrap_or_default(), entry.link().unwrap_or_default());
    Message::builder()
        .from(sender.parse().unwrap())
        .to(recipient.parse().unwrap())
        .subject(entry.title().unwrap_or_default())
        .header(ContentType::TEXT_PLAIN)
        .body(body)
        .unwrap()
}

Aztán jött az email létrehozása a fiókban IMAP protokolon keresztül:

use imap::Client;
use native_tls::TlsConnector;

async fn save_emails_to_mailbox(
    imap_server: &str,
    user_email: &str,
    password: &str,
    mailbox: &str,
    messages: Vec<Message>,
) -> Result<(), Box<dyn std::error::Error>> {
    let tls = TlsConnector::builder().build()?;
    let client = Client::secure_connect((imap_server, 993), imap_server, &tls)?;
    let mut imap_session = client.login(user_email, password).map_err(|(e, _)| e)?;

    imap_session.select(mailbox)?;

    for message in messages {
        let message_string = message.formatted();
        imap_session.append(mailbox, message_string.as_bytes())?;
    }

    imap_session.logout()?;
    Ok(())
}

Itt azért van baj. A Client::secure_connect nem létezik, a message_string-nek nincs as_bytes metódosa és az append(...) után valamiért nem lehet ?-et írni. Ha értenék a Rust-hoz, biztos meg tudnám magyarázni, hogy miért, de ismerve a helyzetemet legyen elég annyi, hogy így már lefordult:

use imap::ClientBuilder;

async fn save_emails_to_mailbox(
    imap_server: &str,
    user_email: &str,
    password: &str,
    mailbox: &str,
    messages: Vec<Message>,
) -> Result<(), Box<dyn std::error::Error>> {
    let client = ClientBuilder::new(imap_server, 993).native_tls()?;
    let mut imap_session = client.login(user_email, password).map_err(|(e, _)| e)?;

    imap_session.select(mailbox)?;

    for message in messages {
        let message_string = message.formatted();
        imap_session.append(mailbox, message_string.as_slice());
    }

    imap_session.logout()?;
    Ok(())
}

És végül, de nem utolsó sorban a main() függvény, ami összedrótozza ezt az egészet:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let rss_url = "https://example.com/rss_feed.xml";
    let imap_server = "imap.gmail.com";
    let user_email = "your_email@example.com";
    let password = "your_password";
    let mailbox = "INBOX";

    let channel = parse_rss_feed(rss_url).await?;

    let messages: Vec<_> = channel
        .items()
        .iter()
        .map(|entry| create_email_from_entry(entry, user_email, user_email))
        .collect();

    save_emails_to_mailbox(imap_server, user_email, password, mailbox, messages).await?;

    Ok(())
}

Ebben nem talált a fordító semmi hibát, viszont előjött egy új warning az előző kódból, hogy az append(...) által visszaadott AppendCmd-t használnunk kellene. Ebből arra következtettem, hogy valami még nem kerek és végül erre jutottam:

imap_session.append(mailbox, message_string.as_slice()).finish()?;

Öröm és boldogság, visszatért a sor végi ? és a kód is lefordult gond nélkül. Még a main()-ben lévő változókat átírtam megfelelő értékekre, megfuttattam a kapott programot és... működött!

A szép új világ

Mondanám, hogy kár volt az ember munkáját egy gépre bízni, de egyrészt még így is rengeteg időt meg lehetett spórolni vele, másrészt a ChatGPT valószínűleg megjavította volna saját maga is, ha visszamásolgatom neki a hibaüzeneteket, amiket kapok.

Utána beszélgettünk még egy kicsit, hogy hogyan futtatná a programot systemd, Supervisor vagy éppen Docker segítségével, hogy hogyan lehetne monitorozni egy ilyen programot, arról, hogy milyen config fájl formátumot ajánlana hozzá, de ha erre is kíváncsiak vagytok, akkor azt már tőle kell megkérdezni, biztos szívesen elmeséli.

This post is also available in english: Rolled by the machine

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.