cybrkyd

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

 Wed, 11 Jun 2025 10:33 UTC
Python script to add a new note to a static HTML page
Image: CC BY 4.0 by cybrkyd

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

  1. User sends themselves an e-mail with subject “post.it”
  2. Script processes the e-mail and extracts:
  3. Plaintext paragraphs
  4. Detected URLs (with metadata)
  5. Attached images
  6. Formats content as:
  7. Embed cards for link-rich posts
  8. Simple HTML blocks for plaintext/image-only posts
  9. Injects result into the blog’s static HTML file

Key Features

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.

»
Tagged in:

Visitors: Loading...