Rolled by the machine

Let ChatGPT do the programming

Midjourney generated this image for us

I'm thinking about this idea for a while now about alternative uses of the mailbox. The base of the idea is that you can create arbitrary email-like things in your account via the IMAP protocol, without actually sending any emails.

For example, we could click on a "Save for later" button on a page, and an unread email with the page's content appears in our mailbox. Or we subscribe to the RSS feed of a site and new articles arrive as emails. So it could be used as kind of a database, for which there are already a bunch of clients on every platform imaginable.

A little bit of extra intelligence

Time went by, but the project was still going nowhere until one day I was talking to ChatGPT (model GPT-4, but I just called him Dave) about what fun weekend projects he could come up with. The responses weren't so inspiring, but it occurred to me that I already have a weekend project I should dust off.

So I asked him how to process RSS feeds in Python. Then how to create email messages and save them via IMAP protocol. The answers were quite convincing at first glance, so I had him write the whole project: create an email message from all the entries of an RSS feed, then save it to a mailbox via IMAP, all written in Python of course.

At this point, I had a minor existential crisis. No need to keep me around, just ask ChatGPT. I'm sure it would take him much less time to write this article and here I am, doing this for hours.

But gloom aside, I finally decided to tell him to rewrite the whole thing in Rust and I'll try to run it. It's a trendy thing anyway and I don't really know Rust, so it'll be more exciting.

Ready to start

First, I got a list of the dependencies I need to add to my Cargo.toml file:

[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"] }

Looks good, but imap doesn't have 3.0.0 (yet), so I changed it to 3.0.0-alpha.10 because that was the most up-to-date version. If it were up to me, I'd rather use the latest stable version of everything, but I'm not paid to think (heck, I'm not even paid).

The first code snippet I got was the downloading and processing of the RSS feed:

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)
}

The compiler complained that it didn't know anything about the reqwest module, so I had to add a reqwest = "0.11.16" row to the Cargo.toml file.

The next piece of code I got was the creation of the email message from the RSS feed:

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()
}

The compiler did not like the use of Header because it's private. And the Mailbox isn't even used. Fortunately, there is built-in support for the text/plain content type in the library, so this is what I came up with:

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()
}

The next thing was saving the email message via IMAP protocol:

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(())
}

That's a bit problematic. The Client::secure_connect doesn't exist, message_string doesn't have an as_bytes method and for some reason, you can't put ? after append(...). If I'd know Rust, I could probably explain to you why, but knowing my situation, let's just say that I managed to compile it eventually:

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(())
}

And last but not least is the main() function, which wraps it all up:

#[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(())
}

The compiler didn't find any errors in this, but a new warning from the previous code came up that we should use AppendCmd returned by the append(...) call. From this I had a suspicion that something was not quite right and fixed it like this:

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

What a joy, the ? at the end of the line is back and the code is compiled without any problems. At last, I replaced the values of the config variables in main() with the correct ones, ran the resulting program, and... it worked!

A brave new world

I would be more than happy to say that it was a shame to leave a human's work to a machine, but on one hand, it still saved a lot of time for me, and on the other hand, ChatGPT would probably have fixed it himself if I had copied back the error messages to him.

Afterward, we talked a bit more about how he would run the program using systemd, Supervisor, or Docker, how to monitor such a program, and what config file format he would recommend, but if you want to know more about that, you'll have to ask him, he'll be happy to tell you.

Ez a bejegyzés magyar nyelven is elérhető: Ezt dobta a gép

Have a comment?

Send an email to the blog at deadlime dot hu address.

Want to subscribe?

We have a good old fashioned RSS feed if you're into that.