Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
52

Plain Text Emails, Please

from Making software better without sacrificing user experience. [alt+shift+b] in programming

Plain Text Emails, Please 2019-09-09 When it comes to website / product design and development most devs should try to keep things simple. By only using as much code as absolutely necessary, projects avoid growing out of scope or becoming bloated. So, why isn't this same approach taken for email? A brief history of email Email has been possible since the 1960s with time-sharing computers being used to share files and messages across early devices. Around the 80s and 90s it seemed as though GOSIP would dominate the market, but this was knocked out in favor of SMTP, POP3 and IMAP in 1995 when the National Science Foundation ended its sponsorship of the Internet backbone, and all traffic relied on commercial networks. The first computers to send and receive email. Source: Wikipedia Things were looking pretty good at this point. Most operating systems now had a shared foundation of sending and receiving emails on the internet, allowing for a set of standards to be slowly developed and agreed upon over time. These were simpler times, with the default content sent between machines being plain text. No embedded images, no CSS3 fallback support, no fluff - just content. Sidenote: the history of the internet if you're into that kind of thing. Looking at some data Data isn't everything I understand that the data being used is currently 16 years old - but not many extensive research studies have been performed (specifically for email-type preference in general) In 2002[1], a small-set survey was run by ClickZ was created to gauge the details of personal email data. The main data we will focus on is the user preference between HTML or plain text formats: Do you prefer receiving HTML or text email? Response Percentage (%) HTML 41.95 Plain Text 31.52 No Preference 26.53 prefers HTML email over plain text (~42% vs ~32%) - but I would disagree with this analysis. The roughly 27% of respondents who answered with No Preference should not be dismissed so easily. Since...
9th Sep 2019

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from Making software better without sacrificing user experience.

Bringing dwm Shortcuts to GNOME

Bringing dwm Shortcuts to GNOME 2023-11-02 The dwm window manager is my standard "go-to" for most of my personal laptop environments. For desktops with larger, higher resolution monitors I tend to lean towards using GNOME. The GNOME DE is fairly solid for my own purposes. This article isn't going to deep dive into GNOME itself, but instead highlight some minor configuration changes I make to mimic a few dwm shortcuts. For reference, I'm running GNOME 45.0 on Ubuntu 23.10 Setting Up Fixed Workspaces When I use dwm I tend to have a hard-set amount of tags to cycle through (normally 4-5). Unfortunately, dynamic rendering is the default for workspaces (ie. tags) in GNOME. For my personal preference I set this setting to fixed. We can achieve this by opening Settings > Multitasking and selecting "Fixed number of workspaces". Screenshot of GNOME's Multitasking Settings GUI Setting Our Keybindings Now all that is left is to mimic dwm keyboard shortcuts, in this case: ALT + $num for switching between workspaces and ALT + SHIFT + $num for moving windows across workspaces. These keyboard shortcuts can be altered under Settings > Keyboard > View and Customize Shortcuts > Navigation. You'll want to make edits to both the "Switch to workspace n" and "Move window to workspace n". Screenshot of GNOME's keyboard shortcut GUI: switch to workspace Screenshot of GNOME's keyboard shortcut GUI: move window to workspace That's it. You're free to include even more custom keyboard shortcuts (open web browser, lock screen, hibernate, etc.) but this is a solid starting point. Enjoy tweaking GNOME!

2nd Nov 2023 • 119 votes
Installing Older Versions of MongoDB on Arch Linux

Installing Older Versions of MongoDB on Arch Linux 2023-09-11 I've recently been using Arch Linux for my main work environment on my ThinkPad X260. It's been great. As someone who is constantly drawn to minimalist operating systems such as Alpine or OpenBSD, it's nice to use something like Arch that boasts that same minimalist approach but with greater documentation/support. Another major reason for the switch was the need to run older versions of "services" locally. Most people would simply suggest using Docker or vmm, but I personally run projects in self-contained, personalized directories on my system itself. I am aware of the irony in that statement... but that's just my personal preference. So I thought I would share my process of setting up an older version of MongoDB (3.4 to be precise) on Arch Linux. AUR to the Rescue You will need to target the specific version of MongoDB using the very awesome AUR packages: yay -S mongodb34-bin Follow the instructions and you'll be good to go. Don't forget to create the /data/db directory and give it proper permissions: mkdir -p /data/db/ chmod -R 777 /date/db What About My "Tools"? If you plan to use MongoDB, then you most likely want to utilize the core database tools (restore, dump, etc). The problem is you can't use the default mongodb-tools package when trying to work with older versions of MongoDB itself. The package will complain about conflicts and ask you to override your existing version. This is not what we want. So, you'll have to build from source locally: git clone https://github.com/mongodb/mongo-tools cd mongodb-tools ./make build Then you'll need to copy the built executables into the proper directory in order to use them from the terminal: cp bin/* /usr/local/bin/ And that's it! Now you can run mongod directly or use systemctl to enable it by default. Hopefully this helps anyone else curious about running older (or even outdated!) versions of MongoDB.

11th Sep 2023 • 81 votes
Converting HEIF Images with macOS Automator

Converting HEIF Images with macOS Automator 2023-07-21 Often times when you save or export photos from iOS to iCloud they often render themselves into heif or heic formats. Both macOS and iOS have no problem working with these formats, but a lot of software programs will not even recognize these filetypes. The obvious step would just be to convert them via an application or online service, right? Not so fast! Wouldn't it be much cleaner if we could simply right-click our heif or heic files and convert them directly in Finder? Well, I've got some good news for you... Basic Requirements You will need to have Homebrew installed You will need to install the libheif package through Homebrew: brew install libheif Creating our custom Automator script For this example script we are going to convert the image to JPG format. You can freely change this to whatever format you wish (PNG, TIFF, etc.). We're just keeping things basic for this tutorial. Don't worry if you've never worked with Automator before because setting things up is incredibly simple. Open the macOS Automator from the Applications folder Select Quick Option from the first prompt Set "Workflow receives current" to image files Set the label "in" to Finder From the left pane, select "Library > Utilities" From the presented choices in the next pane, drag and drop Run Shell Script into the far right pane Set the area "Pass input" to as arguments Enter the following code below as your script and type ⌘-S to save (name it something like "Convert HEIC/HEIF to JPG") for f in "$@" do /opt/homebrew/bin/heif-convert "$f" "${f%.*}.jpg" done Making Edits If you ever have the need to edit this script (for example, changing the default format to png), you will need to navigate to your ~/Library/Services folder and open your custom heif Quick Action in the Automator application. Simple as that. Happy converting! If you're interested, I also have some other Automator scripts available: Batch Converting Images to webp with macOS Automator Convert Files to HTML with macOS Automator Quick Actions

21st Jul 2023 • 56 votes
Running VSCode in Chromium on OpenBSD

Running VSCode in Chromium on OpenBSD 2023-06-05 VSCode and its many variations are not available on OpenBSD. This doesn't cause issue with many OpenBSD users, but those making the jump from Linux might miss access to such a popular editor. Lucky for us, there is a hacky workaround to solve this problem. VSCode in the Browser I tried my best to build something like code-server locally and run that directly in my browser - but I failed miserably. Instead, I fell back on vscode.dev which is essentially a remote version of code-server. Getting things to work seamlessly proved a little more challenging. I found the best performance was running everything through Chromium with special parameters enabled on launch. Note: The following assumes you have already installed chromium First we need to disable unveil for Chromium. This will allow us to access our system files through vscode.dev using the "Open folder..." or "Open file..." commands without issue: chrome --disable-unveil Everything should work pretty solid right out the box now - except it doesn't. Syntax highlighting does not work without enabling WASM/WebAssembly. Your experience might be different, but I had to include the following when launching Chromium from the terminal: ENABLE_WASM=1 chrome --enable-wasm Success! We can avoid typing out these complex commands everytime we want to launch our editor by setting up an alias (in my case via .zshrc): alias vscode="ENABLE_WASM=1 chrome --enable-wasm --disable-unveil" That's it! Now I can just pop open VSCode on OpenBSD by simply running vscode in my terminal. Hopefully this can help others slowly transition over to OpenBSD - which you should do because it is amazing!

5th Jun 2023 • 111 votes
Converting My X201 ThinkPad into a Slabtop

Converting My X201 ThinkPad into a Slabtop 2023-05-01 I recently wrote about physically disabling the WiFi toggle switch on my X201 which was a fun "hack" to an annoying issue I was running into. Since then, the laptop has been running flawlessly. The only other minor issue I had was the poor display quality. The screen works perfectly fine but the X201's age prevents it from being the best possible display for day-to-day tasks. Both the resolution and viewing angles are quite poor. So, what were my options to remedy this problem? Swap out and upgrade the laptop's display (not many options for this though) Connect an external display, keyboard, mouse and dock the X201 Something else? Looking into "Something Else" I found that the possible replacement screens weren't worth the cost/hassle to swap out. And setting the laptop aside, docked with external peripherals sounded fine in theory - but then I would be missing out on the X201's amazing classic keyboard... Then I thought to myself, "Why not just remove the display entirely?" So that's what I did. The X201 display (still inside the top lid) disconnected from the body of the laptop. Removing the entire top lid was easier than I initially thought it would be. The beautiful thing about these older, classic ThinkPads is the ability to completely dissemble them. I won't go into heavy details on how to take one of these machines apart, since great documentation already exists. But once you have the keyboard and hinge screws removed it is essentially as easy as lifting the lid out of the chassis. (After disconnecting the small display connectors, of course) Ditching Alpine for Debian I love Alpine Linux. It's normally my go-to distro (paired with dwm) for most of my laptop devices. Unfortunately, I find tiling window managers a slight pain to use on larger, 4K displays. I also wanted this slabtop to "just work", instead of needing to fiddle around with WiFi, xrandr, audio etc. Not to mention that Debian is rock solid stable. Defaulting to the XFCE DE was a no-brainer as well since this device is somewhat old. The X201 "slabtop" connected to my 27" 4K monitor via the ThinkPad UltraDock. Closing Thoughts This slabtop gives me those old-school "Commodore64" vibes (or more recent devices like the Raspberry Pi 400). This setup certainly isn't for everyone but for my use case it's working well. Traveling with a machine like this might prove a little more challenging - although you could simply pair it with a smaller travel monitor (I'm sure the overall carrying weight would be similar to the standard X201). If nothing else, it is good to keep this option in mind if you ever break your laptop's display and don't feel like spending the money to fix it. Just grab an external monitor you have laying around and make a slabtop!

1st May 2023 • 109 votes

More in programming

Industrial Growth

New electronica album by Warrington-Runcorn New Town Development Plan

28 minutes ago • 1 votes
Verifying my web archives with Playwright

To protect against link rot and disappearing content, I save local copies of web pages I like into my web archive. Ideally, this archive is completely self-contained. I want a local copy of each page’s HTML and all the assets it depends on (CSS, images, scripts, and so on). I don’t want my archived pages fetching resources from the original website, which could disappear at any time. Initially, I was verifying self-containedness by hand. I’d open each saved page in a web browser, and check the browser dev tools. I’d check the Console tab for errors and failed requests, and watch the Network tab for requests going out to the internet. That approach was tedious and error-prone, and I wanted something better. I now automate the process using Playwright, a testing framework that allows you to control web browsers. I open each page using Playwright, and check if it’s making external requests. Detecting external requests in Playwright When you open a page in Playwright, you can subscribe to request events which tell you when the page issues a request. In this Python snippet, we start Playwright, start a WebKit browser, and open a new page. We tell the page to append external requests to a list, and then load example.com. We see that example.com makes two requests on page load: one for https://example.com, and one for a JavaScript file. from contextlib import closing from playwright.sync_api import sync_playwright with ( sync_playwright() as p, closing(p.webkit.launch()) as browser, closing(browser.new_page()) as page, ): requests = [] page.on("request", lambda msg: requests.append(msg)) page.goto("https://example.com") print("requests:", requests) # requests: [<Request url='https://example.com/' method='GET'>, # <Request url='https://example.com/s.js' method='GET'>] In a more complex scenario, we might use Playwright to interact with the page after loading – for example, clicking a button or filling in a form, and then we could see the requests triggered by each action. Once we have a list of requests made by the page, we can inspect the url property to check if any of them were fetching from an external website. I wrote a function is_external_request to carry out this check: from playwright.sync_api import Request import urllib.parse def is_external_request(req: Request, original_url: str) -> bool: """ Check if a request has a different origin than the original URL. A request is considered internal if: - it matches the original URL exactly - it has the same scheme, host, and port as the original URL - it uses a `blob:` scheme (for example, in-memory media representations) """ if req.url == original_url: return False # Compare the scheme/host of the request to that of the original URL. org_url = urllib.parse.urlsplit(original_url) req_url = urllib.parse.urlsplit(req.url) if org_url.scheme == req_url.scheme and org_url.netloc == req_url.netloc: return False # Ignore blob: URLs, which are in-memory representation of some data, # and not an external request. # # Playwright detects blob: URLs when loading media files; for example # using the <audio> or <video> tag. if req_url.scheme == "blob": return False # Fail open: if we can't decide if a request was internal or external, # assume it was external. return True assert_page_is_self_contained(ctx, url) I’ve wrapped these two steps in a function assert_page_is_self_contained(ctx, url), which is part of my chives utility library. The function takes a Playwright Browser (or BrowserContext) and a URL, then: Opens a new page in the browser Loads the URL in the page Checks what happened when the page completes loading It throws an exception if there are any: Uncaught page exceptions or errors Console warnings or errors Non-GET requests Requests targeting external resources The function tries to trigger important network requests that might be skipped on the initial page load. For example, it evaluates the body’s offsetHeight to force a synchronous reflow, which causes the browser to request any lazily loaded images or fonts that might otherwise be missed. Here’s an example which verifies a file example.html from the working directory: from chives.browser_fixtures import assert_page_is_self_contained, file_uri from contextlib import closing from playwright.sync_api import sync_playwright with ( sync_playwright() as p, closing(p.webkit.launch()) as browser, ): assert_page_is_self_contained(browser, url=file_uri("example.html")) I originally passed a Page as the first argument, but changing the function to take a Browser or BrowserContext lets it manage its own pages. It gets a fresh page for every verification, and closes it when it’s done. (The first time I ran this, I created a new page for every verification, and never closed them – my computer ground to a crawl as my script opened hundreds of browser tabs.) If you create a new context, you can emulate different browser settings, like location, locale, and window size. Here’s an example where we use contexts to test the same page in light/dark mode: with ( sync_playwright() as p, closing(p.webkit.launch()) as browser, ): with closing(browser.new_context(color_scheme="light")) as light_ctx: assert_page_is_self_contained(light_ctx, url=file_uri("example.html")) with closing(browser.new_context(color_scheme="dark")) as dark_ctx: assert_page_is_self_contained(dark_ctx, url=file_uri("example.html")) In my web archive, I check every page in light/dark mode, and at 1×/2× pixel densities. It takes at least a second to run each verification, so I cache the results. As I save new pages, they’re added to a verification queue, and I’m warned of any missing assets. Unsurprisingly, the first run flagged many missing fonts and images, which I added to my local archive. Every saved page now passes cleanly. Browser is best-effort, but may still miss things Even with automated testing, it’s difficult to guarantee a completely self-contained archive. Different instances of a browser might load different assets, and a single page load might miss different variations: Responsive images – web pages can offer different sizes of an image, and the browser chooses a specific size based on the current window dimensions Appearance settings – a browser might load different fonts or images depending on light/dark mode Interaction-dependent assets – a web page might have assets that are only required on hover or user action You can mitigate some of this by testing with multiple contexts or adding post-load interactions, but every step makes the process take longer. Fortunately, most of the pages I save are simple, blog-style pages where I’ve stripped out the JavaScript. For my specific use case, initial load verification is more than good enough. Other uses for this code There are other projects where it could be useful to intercept and audit the requests made by a web page: Verifying your Content Security Policy (CSP). The CSP header restricts what resources can be loaded by a web page, and you could check that your configuration has the correct effect. Auditing login screens. You could check that sensitive pages don’t load any external scripts, which mitigates the risk of credential theft via a compromised third-party. Testing analytics and tracking. You could script user flows, like a shopping cart checkout, and confirm that tracking events fire correctly. I don’t do much front-end web development, but Playwright has become a key tool in my toolbox. I read a lot about people using AI agents and MCPs to interact with websites, but I prefer Playwright. It’s more fiddly and requires explicit instructions, but it’s constrained, deterministic, and I never have to worry about it doing something unexpected. [If the formatting of this post looks odd in your feed reader, visit the original article]

an hour ago • 1 votes
Print Sales, Costs, And Profit: 2026

It’s four years since I bought a printer. I thought I would provide an update on running costs, print sales, and profit. I’m happy to be completely transparent about all this as I know it can be difficult to figure this out if you are thinking of creating your own printing business. January 11th 2026, 11:30am All the photos here are work shot in the last twelve months. Year Four Investment? Nothing other than consumables this year. I did purchase a high-spec film scanner last year, detailed here, but don’t consider that as part of this. The scanner has paid for itself several times over, and it may become a factor in future years (see below). Income Income is made up of sales through a local gallery that I am part of, along with web sales: 5,940.- CHF - sales through the gallery 80.- CHF - sales through web shop = 6,020.- CHF total income Total income is down about 50% compared to 2025. Costs I had to purchase consumables (paper and ink), at a total of 957.- CHF. That cost is included in the total here under the “print consumables” category. January 11th 2026, 3:30pm Rental cost of space in the gallery was reduced due to having overpayments of rent and my loan to the gallery, from the previous year(s), paid back to me. -1,500.- CHF - rental cost of space in the gallery -957.- CHF - print consumables (paper, ink) -1,138.- CHF - framing/mounting/postage -1,135.- CHF - commission to the gallery -300.- CHF - web shop subscription 0.- CHF - stripe / paypal fees 0.- CHF - advertising (none this year) = -5,030.- CHF total costs Total costs are down by about 60% compared to 2025. Total Profit 6,020.- CHF total income -5,030.- CHF total costs = 990.- CHF total profit A Quiet Year? Actually an average year. The previous couple of years were outliers, due to large orders, so really I’m back to normal in terms of sales. The key factors in managing to generate a [small] profit this year were: 1) no longer having to pay back the loan used to finance the printer, and 2) reduced rental cost of space in the gallery due to a previous loan to it being repaid. Also having to spend almost 1,000.- CHF on ink this year ate into profit. This is probably a “once every three years” cost given the rate I print at. The principal cost is rental of space in the gallery - in a normal year this would be 3,000.- CHF. Essentially this means, given the material cost of printing and framing/mounting and commission to the gallery, I need to make about 5,500.- CHF of sales to break even. Solar Eclipse, August 12th 2026 That’s a grind. However I did receive two substantial orders at the end of October, which haven’t made it in to this year’s figures and should boost next year’s. Maybe even a good sign for the year to come? At least now the original investment loan is repaid, the printing costs cover themselves in most years. I may look at offering scanning services now that I have a high-spec film scanner, which could become a scan + print service. More thoughts about that next time.

16 hours ago • 1 votes
This RSS feed has moved to a new domain

I have recently moved my website to a new domain. My old posts may reappear in your feed reader — sorry for the inconvenience! I redirect the RSS feed to the new domain, but you might want to update the URL in your reader. I've also merged my dev, photography, and personal posts into a single feed. If you're only interested in one of these categories, you can subscribe to the corresponding feed: All posts Software development posts Photography posts Personal posts I'll soon publish an update on my website redesign — stay tuned!

21 hours ago • 2 votes
Contract testing implementation challenges

This post was previously published through my newsletter on October 19, 2026. From time to time, I will republish newsletter issues on my blog here if I think people (and search engines) might benefit from it. If you want to read everything I’ve written once it is posted, I recommend signing up for the newsletter. For the last 5-6 years or so, I’ve been talking about, teaching and helping teams implement contract testing on a regular basis. To make that more concrete: at the moment, I’m having ongoing conversations with four different potential clients about helping them in some way with their contract testing implementation, and so far, I’ve run my contract testing workshop four times this year. It probably doesn’t come as a surprise to you when I say that I really like the concept of contract testing as a means of addressing integration testing challenges in distributed software systems. However, no matter if I’m speaking, teaching or implementing contract testing, certain challenges almost always come up at some point, and I want to highlight some of those today. Understanding the concept of contract testing First of all, it often takes people a while to understand the concept of contract testing, especially when they’re used to what I call the ‘traditional’ approach to integration testing: deploying the systems and services in scope to a shared testing environment and then testing them ‘as a whole’. Of course, the more distributed your system and your development process is, and the higher the deployment frequency, the more complex and the slower this integration testing approach is going to be. The premise behind contract testing is to break that integration testing puzzle down into small chunks of individual consumer-provider pairs, and then test the integration between those two in an asynchronous manner. By asynchronous, I mean that there is no need to deploy consumer and provider into a shared environment; both parties perform their part of the process in their own, local development environment. This means you can detect potential integration issues before you deploy, instead of stumbling onto them after you do so. This makes contract testing (or contract-based integration testing, to use its proper name) a significantly different approach to integration testing compared to the more ‘traditional’ approach. In my experience, it usually takes people time to wrap their head around how the process actually works, which steps you should take and in which order, and what you (do not yet) know after each step. It often takes going through several contract testing cycles before people start to ‘get it’. That’s not their fault, that is to be expected, since the contract-based approach to integration testing is, again, quite different from what they’ve been doing for so long. To better cater to this, I redesigned my contract testing workshop a year and a half or so ago. It is now a full-day workshop (half a day is simply too short), and I take much smaller steps and repeat them more often. From what I see, this has led to a better understanding of the concepts with my audience, and that’s the whole point. Contract testing === Pact I’ve talked to quite a few teams that had heard of contract testing somewhere, decided that that was the way out of their integration testing problems, and then immediately started writing contract tests with Pact. Understandable, because Pact is a very useful, very powerful and very popular tool, plus, it’s open source, which makes it easy for teams to get started. Often, they skipped a step in the process. Pact is very much a consumer-driven contract testing tool by nature, and while consumer-driven contract testing is the richest and most powerful of the different approaches to contract testing, it is not necessarily the best fit for every team and every context. It’s a classic example of using the wrong tool for the problem at hand, because they didn’t take enough time looking at the problem and the available solutions. That’s not the problem of Pact, even though it is often framed that way (‘Pact didn’t solve my problems, therefore Pact is not a good tool’). That’s a ‘we didn’t fully understand the problem and the context’ problem. Contract testing removes the need for communication Another challenge that I often see when I talk about or help teams with contract testing is the fact that teams don’t talk to each other. This then leads to integration issues, because team A expects something from team B that team B doesn’t supply. Contract testing is not the solution to these problems. It can’t be - tools alone do not solve problems, let alone communication problems. What contract testing does do is make potential integration issues visible early on in the development process, often earlier than (again) more ‘traditional’ approaches to integration testing. That’s where the value of contract testing can be found. It is not a means to replace communication between teams. In fact, and this is especially true for consumer-driven contract testing, contract testing cannot succeed without good communication between consumer and provider teams. Contract testing replaces all other types of integration testing This one comes up less often than it used to, but sometimes, I still hear people saying that when they will have contract-based integration tests in place, they don’t need other types of integration tests anymore - including E2E tests. That’s simply not true. While contract testing is a great way to find out earlier about the impact of a change in a specific consumer or provider further downstream, in many cases, I wouldn’t rely on these tests alone before moving to production. Because of its asynchronous nature, contract-based integration testing only verifies whether a consumer and a provider speak the same language - can a consumer understand the message returned by a provider in a specific situation? It does not verify that the request or the response message reaches the other party in the first place, for example. In other words, it doesn’t test connectivity between a consumer and a provider. And because integration testing is broken down into individual consumer-provider pairs, you don’t see the entire system working ‘as a whole’. This means you might want to keep a few end-to-end tests in place, even when you’ve got good contract testing coverage across the board. The above is not a complete list of challenges with contract testing, but the ones I mentioned here are those that come up most often.

3 days ago • 1 votes
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in