野声

Hey, 野声!

谁有天大力气可以拎着自己飞呀
twitter
github

A free Newsletter to RSS solution

I used to subscribe to newsletters using my own email, but it was quite troublesome to receive so many emails every day, mixed in with reminders and communication emails.

I have also tried several newsletter subscription applications and have explored websites and open-source projects related to mail2rss, but I haven't found a good solution.

One day, I accidentally came across testmail.app, a website that allows developers to test email services. Each user can have a namespace and create unlimited email addresses. There is also an API available to filter emails, among other things.

The free version of testmail.app allows receiving 100 emails per month, with emails being saved for one day. However, by setting the RSS reader's request frequency to less than one day, you can receive all the emails.

So, the idea for the project is to use serverless to implement a function that fetches the latest email list every time the RSS reader requests it, and then responds with an RSS-formatted XML document.

Mermaid Loading...

With the overall idea in mind, we can now start writing the functionality based on the testmail.app API. We will use Cloudflare Workers as the serverless option, which allows up to 100,000 requests per day and unlimited I/O time.

Getting Started with testmail.app#

Terminology#

Once you register and log in, you will see your own namespace.

According to testmail.app, a namespace is described as follows:
testmail.app will receive emails sent to {namespace}.{tag}@inbox.testmail.app. {namespace} is a unique ID assigned to each person, and {tag} is a user-defined value. Later, we can use the API to filter emails based on the tag to retrieve the desired emails.

Namespace#

Each namespace supports an unlimited number of email addresses.

For example, let's say your namespace is "acmeinc". If you send an email to [email protected] and another email to [email protected], you will be able to find both emails under the "acmeinc" namespace. The only difference is the tag associated with each email.

Tag#

A tag can be any content.

For example, if you want to test the registration functionality for new users, you can use the email [email protected] to create a new user with the username "John", and use the email [email protected] to create a new user with the username "Albert".

When querying the API, you can filter emails based on specific tags to view the emails sent to each user.

namespace

Using the API#

API documentation: https://testmail.app/docs/

testmail.app supports both direct parameter queries and GraphQL queries. Both methods return a JSON response containing the email list, email content, attachments, metadata, etc.

Before making a query, you need to obtain an API Key, which can be found after logging in to the website. Add the API Key to the Authorization header.

Here, we won't go into detail, but the basic idea is to make an API request to retrieve the desired email content. For example, if we subscribe to QuartZ using the "quartz" tag, we only need to query the emails with that tag.

Here is an example of a JavaScript function to request emails:

const testmailNamespace = 'xxxxx';
const testmailToken = 'xxxxxxxxxxxxxxx';

class TestMail {
  static testmailApi = 'https://api.testmail.app/api/graphql';

  static async getMails(tag) {
    const query = `{
      inbox (
        namespace: "${testmailNamespace}"
        tag: "${tag}"
        limit: 99
      ) {
        emails {
          id
          subject
          html
          from
          timestamp
          downloadUrl
          attachments {
            cid
            downloadUrl
          }
        }
      }
    }`;

    const init = {
      method: 'POST',
      headers: {
        'content-type': 'application/json;charset=UTF-8',
        Authorization: `Bearer ${testmailToken}`,
        Accept: 'application/json',
      },

      body: JSON.stringify({
        operationName: null,
        query,
        variables: {},
      }),
    };
    return fetch(this.testmailApi, init);
  }
}

Note that we are using the GraphQL request method. Later, you can simply call await TestMail.getMails(tag) to retrieve the emails with the specified tag.

Getting Started with Cloudflare Workers#

According to our plan, we need a serverless function to act as an intermediary layer, generating the RSS content based on the RSS reader's request.

So, we need to:

  1. Parse the tag from the user's request.
  2. Request the email list for the tag.
  3. Return an XML-formatted webpage.

The first step is to parse the user's request URL and extract the tag. For example, if the user requests mail2rss.test.workers.dev/tenjs, we need to extract tenjs.

In Cloudflare Workers, you can use event.request.url to get the complete URL of the user's request. If you are using another serverless service, such as a koa-like framework, you can usually get the URL from the Request object.

const { request } = event;
let url = new URL(request.url);
// parse tag
const requestTag = url.pathname.substring(1);

We have already written the code to request the email list for a specific tag.

The next step is to generate an XML response and return it.

Implementing XML Generation#

Here, we won't be using a template engine. We will simply concatenate strings to generate the XML.

First, we need to understand the RSS file format. We can refer to the RSSHub file content. Then, we can write a makeRss function to concatenate the strings and generate the XML.

async function makeRss(emails, tag) {
  let items = emails.map((value) => {
    if (value.attachments.length > 0) {
      for (let i of value.attachments) {
        // update the image link
        value.html = value.html.replace(`cid:${i.cid}`, i.downloadUrl);
      }
    }
    return `<item>
    <title><![CDATA[${value.subject}]]></title>
    <description><![CDATA[${value.html}]]></description>
    <pubDate>${new Date(value.timestamp).toGMTString()}</pubDate>
    <guid isPermaLink="false">${value.id}</guid>
    <link>${value.downloadUrl}</link>
    <author><![CDATA[${value.from}]]></author>
</item>`;
  });

  return `<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title><![CDATA[${tag} Subscription]]></title>
        <link>${deployUrl + tag}</link>
        <atom:link href="${
          deployUrl + tag
        }" rel="self" type="application/rss+xml" />
        <description><![CDATA[${tag} Subscription]]></description>
        <generator>mail2rss</generator>
        <webMaster>[email protected] (Artin)</webMaster>
        <language>en-us</language>
        <lastBuildDate>${new Date().toGMTString()}</lastBuildDate>
        <ttl>300</ttl>
        ${items.join('\n')}
    </channel>
</rss>`;
}

Then, we can return the response:

let responseXML = await makeRss(data.data.inbox.emails, requestTag);
let response = new Response(responseXML, {
  status: 200,
  headers: {
    'content-type': 'application/xml; charset=utf-8',
  },
});
response.headers.append('Cache-Control', 'max-age=600');
return response;

Summary#

The overall idea and code implementation are quite simple.

You can find the complete code here: https://github.com/bytemain/mail2rss

References#

  1. DIYgod/RSSHub
    https://github.com/DIYgod/RSSHub
  2. testmail.app
    https://testmail.app/
  3. testmail.app Documentation
    https://testmail.app/docs/
  4. Cloudflare Workers
    https://workers.cloudflare.com/
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.