Python script to add a new note to a static HTML page

I would like to have my own “posts”, “tweets”, “notes” in my possession at all times. That way, I hold the raw data and can do whatever I want with it. As we all know, it can take days to get your own post data from some platforms and from others, it is virtually impossible to retrieve unless you have direct access to the database.
Notes was born
I needed to solution something which would be simple to update, so I opted to post a new note via e-mail.
This Python script checks an e-mail inbox for unread messages with the subject “post.it”. If found, the contents of the e-mail are extracted and automatically published to a static HTML page. This is ideal for lightweight, e-mail-driven notes and blogging without a CMS backend.
Workflow
- User sends themselves an e-mail with subject “post.it”
- Script processes the e-mail and extracts:
- Plaintext paragraphs
- Detected URLs (with metadata)
- Attached images
- Formats content as:
- Embed cards for link-rich posts
- Simple HTML blocks for plaintext/image-only posts
- Injects result into the blog’s static HTML file
Key Features
- E-mail Processing
- Connects to IMAP server and fetches unread “post.it” e-mails
-
Parses both plaintext content and image attachments
-
Rich Link Previews
- Detects URLs in the e-mail body
- Fetches Open Graph metadata (title, description, thumbnail)
-
Generates social media-style card previews
-
Image Handling
- Downloads external thumbnails or processes e-mail attachments
- Converts images to AVIF format for optimised web display
-
If image origin is my website, use the existing image
-
Static HTML Integration
- Inserts new posts (formatted as HTML) into
index.html
-
Uses
<!--Marker-->
as the injection point -
Timestamps
- Preserves the e-mail’s original send time for each post
The script
#!/usr/bin/env python3
import requests
import imaplib
import email
from email.header import decode_header
import os
import re
from datetime import datetime
import shutil
from html.parser import HTMLParser
from urllib.parse import urlparse
from pathlib import Path
from subprocess import run
# Email credentials
IMAP_SERVER = 'xxx'
EMAIL_USER = 'xxx'
EMAIL_PASSWORD = 'xxx'
# HTML file path
HTML_FILE_PATH = '/absolute/file/path/to/index.html'
# Image save directory
IMAGE_SAVE_DIR = '/absolute/file/path/to/img-n/'
# URL pattern
URL_PATTERN = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+')
# HTML parser to extract Open Graph metadata
class MetadataParser(HTMLParser):
def __init__(self):
super().__init__()
self.metadata = {"title": None, "description": None, "thumb": None, "site_name": None}
def handle_starttag(self, tag, attrs):
attrs_dict = dict(attrs)
if tag == 'meta':
prop = attrs_dict.get('property') or attrs_dict.get('name')
content = attrs_dict.get('content')
if prop == 'og:title':
self.metadata['title'] = content
elif prop == 'og:description':
self.metadata['description'] = content
elif prop == 'og:image':
self.metadata['thumb'] = content
elif prop == 'og:site_name':
self.metadata['site_name'] = content
# Fetch Open Graph metadata
def fetch_metadata(url):
try:
headers = {'User-Agent': 'Mozilla/5.0'}
r = requests.get(url, headers=headers, timeout=8)
if r.status_code == 200:
parser = MetadataParser()
parser.feed(r.text)
return parser.metadata if any(parser.metadata.values()) else None
except Exception:
pass
return None
# Download and convert image to AVIF
def process_image(url):
try:
r = requests.get(url, stream=True, timeout=8)
if r.status_code == 200:
image_id = os.urandom(8).hex()
ext = Path(urlparse(url).path).suffix
temp_path = f"/tmp/{image_id}{ext}"
final_path = os.path.join(IMAGE_SAVE_DIR, f"{image_id}.avif")
with open(temp_path, 'wb') as f:
for chunk in r.iter_content(1024):
f.write(chunk)
run(['convert', temp_path, '-quality', '80', final_path])
os.remove(temp_path)
return f"{image_id}.avif"
except Exception:
pass
return None
# Mark email as read
def mark_email_as_read(mail, email_id):
mail.store(email_id, '+FLAGS', '\Seen')
# Append plain post to HTML
def append_plain_post(paragraphs, email_date, image_name=None):
content = '<div class="noted-item">\n'
for p in paragraphs:
replaced = p.replace("\n", "<br>\n")
content += f'<p>{replaced}</p>\n'
if image_name:
content += f'<img alt="{image_name}" src="/img-n/{image_name}">\n'
content += f'<p class="noted-post-time">{email_date.strftime("%a, %d %b %Y %H:%M:%S %z")}</p>\n</div>\n'
insert_into_html(content)
# Append social card to HTML
def append_social_card(user_note, url, title, thumb_filename, email_date, site=None, description=None):
content = '<div class="noted-item">\n'
if user_note:
replaced = user_note.replace("\n", "<br>\n")
content += f'<p>{replaced}</p>\n'
content += '<div class="noted-card-wrapper">\n'
content += f'<a href="{url}" target="_blank" class="noted-card">\n'
img_src = thumb_filename if thumb_filename.startswith("http") else f"/img-n/{thumb_filename}"
img_alt = title or "Link preview"
content += f'<img src="{img_src}" alt="{img_alt}" class="noted-card-image" />\n'
if site:
content += f'<p class="noted-card-url">{site}</p>\n'
if title:
content += f'<p class="noted-card-title">Python script to add a new note to a static HTML page</p>\n'
if description:
desc = description.replace("\n", " ").strip()
content += f'<p class="noted-card-description">{desc}</p>\n'
content += '</a>\n</div>\n'
content += f'<p class="noted-post-time">{email_date.strftime("%a, %d %b %Y %H:%M:%S %z")}</p>\n</div>\n'
insert_into_html(content)
# Insert generated HTML block into index.html
def insert_into_html(block):
with open(HTML_FILE_PATH, 'r') as f:
html = f.read()
marker = '<!--Marker-->'
idx = html.find(marker)
if idx != -1:
new_html = html[:idx + len(marker)] + '\n' + block + html[idx + len(marker):]
with open(HTML_FILE_PATH, 'w') as f:
f.write(new_html)
# Main function to process email and post to HTML
def check_email_and_post():
mail = imaplib.IMAP4_SSL(IMAP_SERVER)
mail.login(EMAIL_USER, EMAIL_PASSWORD)
mail.select("inbox")
status, messages = mail.search(None, '(UNSEEN SUBJECT "post.it")')
email_ids = messages[0].split()
for email_id in email_ids:
res, msg = mail.fetch(email_id, "(RFC822)")
for response_part in msg:
if isinstance(response_part, tuple):
msg = email.message_from_bytes(response_part[1])
subject, encoding = decode_header(msg["Subject"])[0]
if isinstance(subject, bytes):
subject = subject.decode(encoding)
if subject.lower() == "post.it":
tweet_text = ""
media_path = None
image_name = None
email_date = datetime.now()
paragraphs = []
for part in msg.walk():
if part.get_content_type() == "text/plain":
email_date = email.utils.parsedate_to_datetime(msg["Date"])
tweet_text = part.get_payload(decode=True).decode(part.get_content_charset())
body = tweet_text.replace('\r', '')
paragraphs = [p.strip() for p in body.split('\n\n') if p.strip()]
elif part.get_content_type().startswith("image"):
filename = part.get_filename()
if filename:
image_name = filename
media_path = os.path.join("/tmp", filename)
with open(media_path, "wb") as f:
f.write(part.get_payload(decode=True))
if media_path:
shutil.move(media_path, os.path.join(IMAGE_SAVE_DIR, image_name))
mark_email_as_read(mail, email_id)
# Check for URL in body
urls = URL_PATTERN.findall(tweet_text)
if urls:
url = urls[0]
meta = fetch_metadata(url)
desc_text = re.sub(URL_PATTERN, '', tweet_text).strip()
if meta:
title = meta.get("title") or "No title"
thumb = meta.get("thumb") or "fallback.avif"
site = meta.get("site_name")
thumb_filename = thumb if thumb.startswith('https://cybrkyd.com') else process_image(thumb)
if not thumb_filename:
thumb_filename = "fallback.avif"
append_social_card(
user_note=desc_text,
url=url,
title=title,
thumb_filename=thumb_filename,
email_date=email_date,
site=site,
description=meta.get("description")
)
else:
append_plain_post(paragraphs, email_date)
else:
append_plain_post(paragraphs, email_date, image_name if media_path else None)
mail.logout()
# Run the main function
check_email_and_post()
And that’s it. Check out my Notes where this is currently deployed.