JavaScript/TypeScript向けの次世代メールライブラリ、Upyo 0.6.0のリリースに合わせて、Nodemailerのような定番のライブラリがある中でなぜUpyoを選ぶ価値があるのかを書きました。
zenn.dev


@hongminhee@hollo.social
1,113 following1,900 followers
An intersectionalist, feminist, and socialist living in Seoul (UTC+09:00). @tokolovesme's spouse. Who's behind @fedify, @hollo, and @botkit. Write some free software in #TypeScript, #Haskell, #Rust, & #Python. They/them.
서울에 사는 交叉女性主義者이자 社會主義者. 金剛兔(@tokolovesme)의 配偶者. @fedify, @hollo, @botkit 메인테이너. #TypeScript, #Haskell, #Rust, #Python 等으로 自由 소프트웨어 만듦.
JavaScript/TypeScript向けの次世代メールライブラリ、Upyo 0.6.0のリリースに合わせて、Nodemailerのような定番のライブラリがある中でなぜUpyoを選ぶ価値があるのかを書きました。
zenn.dev
JavaScript/TypeScript를 爲한 次世代 이메일 라이브러리인 Upyo 0.6.0의 릴리스를 맞이하여, Nodemailer 같은 有力한 旣成 라이브러리를 두고 왜 Upyo를 쓰는 게 좋은지 說明하는 글을 써 보았습니다.
https://hackers.pub/@hongminhee/2026/upyo-email-decoupled-from-where-and-how/ko
There's already an answer to how you send email from Node.js. Node.js has Nodemailer, and it's been a solid choice for a long time. But the code people write today doesn't only run on Node.js anymore. It's common to see it running on Deno, Bun, or an edge runtime like Cloudflare Workers, and it's common to switch providers between development and production: send nothing locally, then use SES or Resend once deployed.
So I wanted to change the question a little. Not how do you send email from Node.js, but how does an application deliver email regardless of runtime or provider. That's the question Upyo started from.
If you're sending email over SMTP from Node.js, Nodemailer is enough. Its SMTP implementation is mature, it supports OAuth 2.0, and it already handles DKIM signing and calendar invitations.
The catch is that Nodemailer runs on top of Node.js built-in modules like node:net, node:tls, and node:stream. That makes it hard to use on Cloudflare Workers, Vercel Edge Runtime, or Supabase Edge Functions. Its provider support centers on SMTP too, so using an HTTP API service like Resend or SendGrid means installing that service's own SDK or relying on a third-party transport. Switching providers usually means touching application code too.
Upyo is built on web standard APIs like fetch(), Web Streams, and Web Crypto. Since it doesn't depend on Node.js specific modules, the same code runs unchanged on Node.js, Deno, Bun, and edge functions.
Every email service is handled through the same Transport interface: SMTP and JMAP, Resend, SendGrid, Mailgun, Amazon SES, Plunk, Lettermint. Swap out how the transport is constructed and the application code that sends mail stays the same, whether that's a local SMTP server or a mock transport in development, or SES in production.
Keeping dependencies minimal follows from the same goal. @upyo/ses implements AWS Signature v4 itself instead of pulling in the AWS SDK. The AWS SDK assumes Node.js, and using it as is would make the transport unusable on Deno, Bun, or an edge runtime.
The Transport interface itself is small. send() sends one message, sendMany() sends several, and cancellation goes through a standard AbortSignal.
How sendMany() is implemented differs by transport. The transport for a provider like Resend or SendGrid that exposes a batch endpoint calls that endpoint directly. SMTP, where reusing one connection for several messages is the natural approach, does that instead. A transport without either optimization might still run several send() calls concurrently instead of one after another, to cut down total time.
Because the interface is this small, the answer to where retry policy should live changes. Wrap a transport in RetryTransport and application code never needs its own retry logic.
That's possible because application code only knows the Transport interface type, not a concrete implementation. The function that sends mail takes a transport: Transport parameter, and the caller decides which transport gets injected.
import { MockTransport } from "@upyo/mock";
import { RetryTransport } from "@upyo/retry";
const provider = new MockTransport();
const transport = new RetryTransport(provider, {
maxAttempts: 3,
backoff: {
baseDelayMilliseconds: 1000,
maxDelayMilliseconds: 30000,
factor: 2,
},
});
RetryTransport implements the same Transport interface, so application code doesn't need to know whether it's talking to SMTP or SES, or whether retries are even happening. Wrap it in PoolTransport and a failing provider can fail over to the next one by priority, or traffic can be spread across several with round robin.
import { PoolTransport } from "@upyo/pool";
const pool = new PoolTransport({
strategy: "priority",
transports: [
{ transport: primaryProvider, priority: 100 },
{ transport: backupProvider, priority: 10 },
],
maxRetries: 3,
});
The two can be stacked. Whether you retry per provider before failing over to the next, or retry the whole pool as a single operation, comes down to which one sits on the outside.
This composition works because every transport returns failures in the same shape. Upyo's Receipt is a discriminated union of success and failure, and a failure carries structured fields like retryable, category, and retryAfterMilliseconds. Once each transport translates a provider's error into these fields, RetryTransport can decide whether to retry a 429 from Resend or a transient SMTP error the same way.
The annoying part of building magic-link login was never the login itself. It was opening an inbox every time and waiting for the link to show up, when the link never needed to be delivered anywhere during development.
The function that sends mail was written to take a Transport from the start.
import { createMessage } from "@upyo/core";
import type { Transport } from "@upyo/core";
async function sendMagicLink(transport: Transport, email: string, link: string) {
await transport.send(createMessage({
from: "auth@example.com",
to: email,
subject: "Your login link",
content: { text: `Click to log in: ${link}` },
}));
}
@upyo/logtape wraps another transport and logs instead of sending. In development, that's what gets injected.
import { LogTapeTransport } from "@upyo/logtape";
import { SmtpTransport } from "@upyo/smtp";
const smtp = new SmtpTransport({
host: "smtp.example.com",
port: 587,
auth: { user: "smtp-user", pass: "smtp-password" },
});
const transport = process.env.NODE_ENV === "development"
? new LogTapeTransport({ category: ["app", "email"] })
: new LogTapeTransport({ transport: smtp, category: ["app", "email"] });
Copy the link straight out of the server log and paste it into the browser. No refreshing an inbox.
Automated tests use the same injection point. This time, sendMagicLink() gets @upyo/mock's MockTransport instead.
import { MockTransport } from "@upyo/mock";
import assert from "node:assert/strict";
const transport = new MockTransport();
await sendMagicLink(transport, "user@example.com", "https://example.com/verify?token=...");
const sent = await transport.waitForMessage(
(msg) => msg.subject.includes("login link"),
1000,
);
assert.equal(sent.recipients[0].address, "user@example.com");
sendMagicLink() itself never changed. Only the Transport handed to it did.
A small Transport interface doesn't mean the messy parts of email got skipped. The SMTP transport handles internationalized addresses through SMTPUTF8 and requests delivery status notifications through DSN. Attachments stream, so even large files don't sit in memory. The Message.calendar field turns a message into a meeting invitation or cancellation, and messageId, inReplyTo, and references let outgoing mail be part of a conversation rather than a one-off notice.
To send over SMTP, install these packages.
npm add @upyo/core @upyo/smtp
import { createMessage } from "@upyo/core";
import { SmtpTransport } from "@upyo/smtp";
const transport = new SmtpTransport({
host: "smtp.example.com",
port: 587,
auth: { user: "smtp-user", pass: "smtp-password" },
});
const message = createMessage({
from: "sender@example.com",
to: "recipient@example.com",
subject: "Hello from Upyo",
content: { text: "This is a test email." },
});
await transport.send(message);
Layer RetryTransport, PoolTransport, or LogTapeTransport on top as needed. Configuration for each transport is in the docs, the source is on GitHub, and packages are on npm and JSR.
That's the answer so far to the question I started with: how an application delivers email regardless of runtime or provider. There's still plenty to refine, so if something's broken or missing, an issue or a discussion is welcome.
jsr.io
@upyo/core on JSR: Simple email sending library for Node.js, Deno, Bun, and edge functions
To mark the release of Upyo 0.6.0, a next-generation email library for JavaScript/TypeScript, I wrote a post on why it's worth choosing over an established library like Nodemailer.
https://hackers.pub/@hongminhee/2026/upyo-email-decoupled-from-where-and-how
hackers.pub
There's already an answer to how you send email from Node.js. Node.js has Nodemailer, and it's been a solid choice for a long time. But the code people write today doesn't only run on Node.js anymore. It's common to see it running on Deno, Bun, or an edge runtime like Cloudflare Workers, and it's common to switch providers between development and production: send nothing locally, then use SES or Resend once deployed.
So I wanted to change the question a little. Not how do you send email from Node.js, but how does an application deliver email regardless of runtime or provider. That's the question Upyo started from.
If you're sending email over SMTP from Node.js, Nodemailer is enough. Its SMTP implementation is mature, it supports OAuth 2.0, and it already handles DKIM signing and calendar invitations.
The catch is that Nodemailer runs on top of Node.js built-in modules like node:net, node:tls, and node:stream. That makes it hard to use on Cloudflare Workers, Vercel Edge Runtime, or Supabase Edge Functions. Its provider support centers on SMTP too, so using an HTTP API service like Resend or SendGrid means installing that service's own SDK or relying on a third-party transport. Switching providers usually means touching application code too.
Upyo is built on web standard APIs like fetch(), Web Streams, and Web Crypto. Since it doesn't depend on Node.js specific modules, the same code runs unchanged on Node.js, Deno, Bun, and edge functions.
Every email service is handled through the same Transport interface: SMTP and JMAP, Resend, SendGrid, Mailgun, Amazon SES, Plunk, Lettermint. Swap out how the transport is constructed and the application code that sends mail stays the same, whether that's a local SMTP server or a mock transport in development, or SES in production.
Keeping dependencies minimal follows from the same goal. @upyo/ses implements AWS Signature v4 itself instead of pulling in the AWS SDK. The AWS SDK assumes Node.js, and using it as is would make the transport unusable on Deno, Bun, or an edge runtime.
The Transport interface itself is small. send() sends one message, sendMany() sends several, and cancellation goes through a standard AbortSignal.
How sendMany() is implemented differs by transport. The transport for a provider like Resend or SendGrid that exposes a batch endpoint calls that endpoint directly. SMTP, where reusing one connection for several messages is the natural approach, does that instead. A transport without either optimization might still run several send() calls concurrently instead of one after another, to cut down total time.
Because the interface is this small, the answer to where retry policy should live changes. Wrap a transport in RetryTransport and application code never needs its own retry logic.
That's possible because application code only knows the Transport interface type, not a concrete implementation. The function that sends mail takes a transport: Transport parameter, and the caller decides which transport gets injected.
import { MockTransport } from "@upyo/mock";
import { RetryTransport } from "@upyo/retry";
const provider = new MockTransport();
const transport = new RetryTransport(provider, {
maxAttempts: 3,
backoff: {
baseDelayMilliseconds: 1000,
maxDelayMilliseconds: 30000,
factor: 2,
},
});
RetryTransport implements the same Transport interface, so application code doesn't need to know whether it's talking to SMTP or SES, or whether retries are even happening. Wrap it in PoolTransport and a failing provider can fail over to the next one by priority, or traffic can be spread across several with round robin.
import { PoolTransport } from "@upyo/pool";
const pool = new PoolTransport({
strategy: "priority",
transports: [
{ transport: primaryProvider, priority: 100 },
{ transport: backupProvider, priority: 10 },
],
maxRetries: 3,
});
The two can be stacked. Whether you retry per provider before failing over to the next, or retry the whole pool as a single operation, comes down to which one sits on the outside.
This composition works because every transport returns failures in the same shape. Upyo's Receipt is a discriminated union of success and failure, and a failure carries structured fields like retryable, category, and retryAfterMilliseconds. Once each transport translates a provider's error into these fields, RetryTransport can decide whether to retry a 429 from Resend or a transient SMTP error the same way.
The annoying part of building magic-link login was never the login itself. It was opening an inbox every time and waiting for the link to show up, when the link never needed to be delivered anywhere during development.
The function that sends mail was written to take a Transport from the start.
import { createMessage } from "@upyo/core";
import type { Transport } from "@upyo/core";
async function sendMagicLink(transport: Transport, email: string, link: string) {
await transport.send(createMessage({
from: "auth@example.com",
to: email,
subject: "Your login link",
content: { text: `Click to log in: ${link}` },
}));
}
@upyo/logtape wraps another transport and logs instead of sending. In development, that's what gets injected.
import { LogTapeTransport } from "@upyo/logtape";
import { SmtpTransport } from "@upyo/smtp";
const smtp = new SmtpTransport({
host: "smtp.example.com",
port: 587,
auth: { user: "smtp-user", pass: "smtp-password" },
});
const transport = process.env.NODE_ENV === "development"
? new LogTapeTransport({ category: ["app", "email"] })
: new LogTapeTransport({ transport: smtp, category: ["app", "email"] });
Copy the link straight out of the server log and paste it into the browser. No refreshing an inbox.
Automated tests use the same injection point. This time, sendMagicLink() gets @upyo/mock's MockTransport instead.
import { MockTransport } from "@upyo/mock";
import assert from "node:assert/strict";
const transport = new MockTransport();
await sendMagicLink(transport, "user@example.com", "https://example.com/verify?token=...");
const sent = await transport.waitForMessage(
(msg) => msg.subject.includes("login link"),
1000,
);
assert.equal(sent.recipients[0].address, "user@example.com");
sendMagicLink() itself never changed. Only the Transport handed to it did.
A small Transport interface doesn't mean the messy parts of email got skipped. The SMTP transport handles internationalized addresses through SMTPUTF8 and requests delivery status notifications through DSN. Attachments stream, so even large files don't sit in memory. The Message.calendar field turns a message into a meeting invitation or cancellation, and messageId, inReplyTo, and references let outgoing mail be part of a conversation rather than a one-off notice.
To send over SMTP, install these packages.
npm add @upyo/core @upyo/smtp
import { createMessage } from "@upyo/core";
import { SmtpTransport } from "@upyo/smtp";
const transport = new SmtpTransport({
host: "smtp.example.com",
port: 587,
auth: { user: "smtp-user", pass: "smtp-password" },
});
const message = createMessage({
from: "sender@example.com",
to: "recipient@example.com",
subject: "Hello from Upyo",
content: { text: "This is a test email." },
});
await transport.send(message);
Layer RetryTransport, PoolTransport, or LogTapeTransport on top as needed. Configuration for each transport is in the docs, the source is on GitHub, and packages are on npm and JSR.
That's the answer so far to the question I started with: how an application delivers email regardless of runtime or provider. There's still plenty to refine, so if something's broken or missing, an issue or a discussion is welcome.
jsr.io
@upyo/core on JSR: Simple email sending library for Node.js, Deno, Bun, and edge functions
오 'フツウの在日'(평범한 자이니치)라는 만화가 연재되고 있었구나? 일본에서 일상을 살아가는 자이니치 주인공들을 다루는 옴니버스 만화인 듯. https://comic-walker.com/detail/KC_012728_S?episodeType=first
comic-walker.com
それはきっと、“わかり合えない”物語じゃない。「在日」コリアンたちの、フツウでちょっとだけフツウじゃない日常。名前、家族、言葉、距離感――誰もが一度は覚えたことのある、小さな違和感の話。いまの「日本で暮らす」を見つめる、オムニバス連作。
I've released Upyo 0.6.0, an email library for JavaScript and TypeScript that works across Node.js, Deno, Bun, and edge runtimes.
This version adds MIME composition without sending, streaming attachments over SMTP, and calendar invitations. You can also set message identifiers for tracking replies and check SMTP or JMAP configuration without sending a test email.
There are new Maileroo and Mailtrap transports, plus a LogTape integration for logging delivery or trying out email workflows locally.
github.com
Upyo is a cross-runtime email library for JavaScript that provides a unified API for sending email across Node.js, Deno, Bun, and edge functions. It supports SMTP, JMAP, and HTTP email providers th...
Why Function Arguments Are Not Function Colors via @abnv https://lobste.rs/s/tsfs3w #go #haskell #programming
https://jerf.org/iri/post/2026/func_args_are_not_colors/
jerf.org
@bootlegrydia Oh, that's interesting. I've actually never even seen the character 氪 before.
Drew DeVault (@drew) has compiled a sourced list tracking prominent figures in the F/OSS community and their controversial/far-right political ties and conduct:
https://drewdevault.com/weird-guys/
Quite an illuminating look at the dynamics and power structures across the F/OSS ecosystem.
drewdevault.com
@cwboden I'd only ever heard of SyncThing by name, but I see now that this is exactly the kind of situation it's for! Thanks for letting me know.
I wish something like Android's Quick Share or Apple's AirDrop would get standardized so we could easily transfer files regardless of the platform. I wonder if there's already a standard for that?
en.wikipedia.org
JSConf JP 2026からCFPの不採択メールが来た。でもCFPを二つ出したのに、不採択メールは一通しか来なかった。もしかして、もう一つのCFPは通ったのかな?とりあえず、もう少しだけ待ってみることにしよう…
@Yoxem 在韓語中,【死語】似乎也同時具備這兩種含義。現代韓語詞彙受日語影響很深,所以我認為這可能是受了日語的影響。
順帶一提,在《標準國語大辭典》中,【死語】被定義為「過去曾使用但現在已不再使用的語言。或指這類單詞。例如古希臘語、古拉丁語等」。
死語不指extinct language而指obsolete vocabulary應該是受日語影響?日語有時候稱詞為語
今週 日曜日(6日)에 瑞草驛 近處에 位置한 오픈업 센터(네이버地圖, 카카오맵)에서 @fedify 寄與를 爲한 모임이 열립니다. 午前 10時에서 午後 6時까지 進行되니, 關心 있는 분들은 自由롭게 오셔서 參與 바랍니다! (10時에서 18時 사이에 아무 때나 오셔서 아무 때나 가셔도 됩니다!)
place.map.kakao.com
서울 서초구 서초대로40길 83 2층
都庁前に総勢22名のDJら集結、関東大震災朝鮮人犠牲者へ追悼文を送らない小池都知事に抗議 - 音楽ナタリー
https://natalie.mu/music/news/687676
「社会は新しい未来を生み出すのではなく、古い暴力をリミックスし続けている。2026年現在ガザで続いている虐殺も、植民地主義という同じループの変奏に他ならない。」
"사회는 새로운 미래를 만들어내는 대신, 낡은 폭력을 계속해서 리믹스하고 있다. 2026년 현재까지도 계속되고 있는 가자 지구의 학살은 바로 이 식민주의라는 동일한 루프의 또 다른 변주에 다름 아니다."
"Rather than generating a new future, society keeps remixing old violence. The massacre still unfolding in Gaza as of 2026 is nothing other than another variation on this same loop of colonialism."
@jj1bdx.tokyo 私も同じ感覚です。韓国語でもこの意味の変化が起きたのは、やはりこの10年ぐらいからだと思います。日本語の漢語は韓国語でもそのまま漢字語として受け入れられやすいせいか、こういう言葉の変化が連動して起きることが結構多い気がします。
ja.wikipedia.org
本来「課金する」という言葉は「料金を課す」という意味だが、近年モバイルゲームなどの影響で「料金を払う」という意味にまで拡大しているという趣旨の日本語記事。ちなみに韓国語でも(おそらく日本のモバイルゲーム、あるいはその影響を受けた韓国・中国のモバイルゲームの影響で)「課金하다」という言葉が「料金を払う」という意味まで含むようになる現象が同じく起きている。
salon.mainichi-kotoba.jp
お金を支払うことについて「課金」を使うか。回答は「使う/使わない」がほぼ半々に分かれました。新聞では紛らわしいとして、支払うことについては「課金」を使わないようにしていますが、辞書には新しい意味として載せるものもあります。
'과금'이 원래는 판매자가 요금을 매기는 의미인데 최근에는 소비자가 돈을 내는 의미로 확장되고 있다. 비슷한 사례로 '모금'은 본래는 돈을 요청하는 뜻이었는데 기부하는 의미가 새로 붙었고. https://salon.mainichi-kotoba.jp/archives/280630
salon.mainichi-kotoba.jp
お金を支払うことについて「課金」を使うか。回答は「使う/使わない」がほぼ半々に分かれました。新聞では紛らわしいとして、支払うことについては「課金」を使わないようにしていますが、辞書には新しい意味として載せるものもあります。
LogTapeを日本語で紹介する記事が公開されました。「ライブラリの中では黙って待つ」という設計思想を軸に、configure()を呼ぶまでログが一切出力されない仕組みや、ゼロ依存・5.3KBでNode.js・Deno・Bun・ブラウザ・エッジランタイムを横断して動く点、階層的カテゴリや構造化ログの実践的な使い方まで、実際に動かせるサンプル付きで解説してくださっています。設計の背景まで汲み取ってもらえて嬉しいです。
easegis.jp
ゼロ依存でNode.js・Deno・Bun・ブラウザ・Edge Runtimeすべてに対応するロギングライブラリ、LogTapeの基本から実践的な使い方まで解説します。
@Profpatsch To be honest, I haven't decided yet, but I'm leaning toward Bitwarden. I'm still thinking it over a bit after reading this post, though.
xn--gckvb8fzb.com
A review of my experience with Bitwarden after several years of self-hosting it, and why I decided to move away from the password manager.
After almost fifteen years, I'm done with @1password. I just read through the email exchange @mvsde posted, where 1Password's support team defended the company's patronage of DHH's Omacom Foundation with the usual “we're funding the foundation, not the individual” line. That distinction doesn't hold up when the money still legitimizes him and everyone his politics attract. I've trusted 1Password with my passwords for longer than most of my relationships have lasted, and that's exactly why this matters: loyalty like that shouldn't be free. Migration starts this week.

Screenshot of an email to 1Password support: Hi, I'm concerned about 1Password sponsoring Omarchy and DHH. 1Password has positioned itself in support of diversity and inclusion. DHH on the other hand is strongly opposed to such values and has become increasingly far right. https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/ As a 1Password customer who is also queer and such the target of DHH's anti-DEI crusade, I have to think about whether I still want 1Password to receive my money. Since it's apparently directly funneled to far right white supremacists as DHH. Regards, Fynn Ellie Becker
@tooearly 이거 페디버스 연동도 되는 거예요?
@sl007 These vocabularies should be implemented in Fedify. See also the docs on extending the vocabulary. Could you file an issue for this?
fedify.dev
The Activity Vocabulary is a collection of type-safe objects that represent the Activity Vocabulary and the vendor-specific extensions. This section explains the key features of the objects.
I think this is the most sophisticated and rigorous piece on the philosophy of technology I've read recently. It was a bit long and challenging to get through, but I'm glad I read it. I highly recommend it to anyone working in the software industry.
New article: this time, we use Heidegger to explain why the "it's just a tool" line that often comes up in tech is so very silly:
https://deadsimpletech.com/blog/no-such-thing-as-just-a-tool
deadsimpletech.com
Heidegger thinks about tools like so: a tool is something (a thing that has being in the Heideggerian sense, not necessarily an object) that represents an extension of human capabilities in some way. A tool is thus any kind of thing that *lets you do something you wouldn't otherwise have been able to*. The important (for us) conceptual leap here is that when a tool works well, or isn't broken, it develops what Heidegger describes as a "ready-to-hand" quality: the tool fades into the background as a kind of human-tool gestalt of a human with the additional capability afforded by a tool forms. As a simple example of this, consider eating with a fork. When the fork is well-designed and not broken, *you don't explicitly have "I am using a fork" in your conscious mind while eating dinner*.
New article: this time, we use Heidegger to explain why the "it's just a tool" line that often comes up in tech is so very silly:
https://deadsimpletech.com/blog/no-such-thing-as-just-a-tool
deadsimpletech.com
Heidegger thinks about tools like so: a tool is something (a thing that has being in the Heideggerian sense, not necessarily an object) that represents an extension of human capabilities in some way. A tool is thus any kind of thing that *lets you do something you wouldn't otherwise have been able to*. The important (for us) conceptual leap here is that when a tool works well, or isn't broken, it develops what Heidegger describes as a "ready-to-hand" quality: the tool fades into the background as a kind of human-tool gestalt of a human with the additional capability afforded by a tool forms. As a simple example of this, consider eating with a fork. When the fork is well-designed and not broken, *you don't explicitly have "I am using a fork" in your conscious mind while eating dinner*.
The dates for FOSDEM 2027 have been confirmed for next year, January 30th–31st (the full weekend). Is anyone planning on going?
The @fedify team, including myself, @z9mb1, and @2chanhaeng, will likely all be heading there together.
fosdem.org
#FOSDEM will happen on the weekend of 2027-01-30 and 2027-01-31!
We'll post a proper news item once all the automations work after our yearly archiving cutover.
fosdem.org
#FOSDEM will happen on the weekend of 2027-01-30 and 2027-01-31!
We'll post a proper news item once all the automations work after our yearly archiving cutover.
fosdem.org
@dansup Making a brand new account just to be mean about someone's dog is pretty sad. Your dog is lovely.