{
  "submission_id": "3517775",
  "keywords": [
    {
      "keyword_id": "14319",
      "keyword_name": "code",
      "contributed": "f",
      "submissions_count": "133"
    },
    {
      "keyword_id": "569",
      "keyword_name": "inkbunny",
      "contributed": "f",
      "submissions_count": "1269"
    },
    {
      "keyword_id": "24188",
      "keyword_name": "program",
      "contributed": "f",
      "submissions_count": "226"
    },
    {
      "keyword_id": "11078",
      "keyword_name": "python",
      "contributed": "f",
      "submissions_count": "1025"
    },
    {
      "keyword_id": "6695",
      "keyword_name": "utility",
      "contributed": "f",
      "submissions_count": "21"
    }
  ],
  "hidden": "f",
  "scraps": "f",
  "favorite": "f",
  "favorites_count": "1",
  "create_datetime": "2025-01-05 13:49:51.091245+00",
  "create_datetime_usertime": "05 Jan 2025 14:49 CET",
  "last_file_update_datetime": "2025-01-05 13:35:19.638684+00",
  "last_file_update_datetime_usertime": "05 Jan 2025 14:35 CET",
  "username": "JustLurking",
  "user_id": "11985",
  "user_icon_file_name": "96044_JustLurking_final_spiral2.gif",
  "user_icon_url_large": "https://nl1.ib.metapix.net/usericons/large/96/96044_JustLurking_final_spiral2.gif",
  "user_icon_url_medium": "https://nl1.ib.metapix.net/usericons/medium/96/96044_JustLurking_final_spiral2.gif",
  "user_icon_url_small": "https://nl1.ib.metapix.net/usericons/small/96/96044_JustLurking_final_spiral2.gif",
  "file_name": "5376851_JustLurking_ib-maildir-fetch.txt",
  "file_url_full": "https://nl1.ib.metapix.net/files/full/5376/5376851_JustLurking_ib-maildir-fetch.txt",
  "file_url_screen": "https://nl1.ib.metapix.net/files/screen/5376/5376851_JustLurking_ib-maildir-fetch.txt",
  "file_url_preview": "https://nl1.ib.metapix.net/files/preview/5376/5376851_JustLurking_ib-maildir-fetch.txt",
  "thumbnail_url_huge": "https://nl1.ib.metapix.net/thumbnails/huge/5376/5376851_JustLurking_ib-maildir-fetch.jpg",
  "thumbnail_url_large": "https://nl1.ib.metapix.net/thumbnails/large/5376/5376851_JustLurking_ib-maildir-fetch.jpg",
  "thumbnail_url_medium": "https://nl1.ib.metapix.net/thumbnails/medium/5376/5376851_JustLurking_ib-maildir-fetch.jpg",
  "thumb_huge_x": "100",
  "thumb_huge_y": "100",
  "thumb_large_x": "100",
  "thumb_large_y": "100",
  "thumb_medium_x": "100",
  "thumb_medium_y": "100",
  "files": [
    {
      "file_id": "5376851",
      "file_name": "5376851_JustLurking_ib-maildir-fetch.txt",
      "file_url_full": "https://nl1.ib.metapix.net/files/full/5376/5376851_JustLurking_ib-maildir-fetch.txt",
      "file_url_screen": "https://nl1.ib.metapix.net/files/screen/5376/5376851_JustLurking_ib-maildir-fetch.txt",
      "file_url_preview": "https://nl1.ib.metapix.net/files/preview/5376/5376851_JustLurking_ib-maildir-fetch.txt",
      "mimetype": "text/plain",
      "submission_id": "3517775",
      "user_id": "11985",
      "submission_file_order": "0",
      "full_size_x": null,
      "full_size_y": null,
      "screen_size_x": null,
      "screen_size_y": null,
      "preview_size_x": null,
      "preview_size_y": null,
      "initial_file_md5": "a9703eac55eae903380b80fcd32098fc",
      "full_file_md5": "a9703eac55eae903380b80fcd32098fc",
      "large_file_md5": "",
      "small_file_md5": "",
      "thumbnail_md5": "40f9f166c8797ff7bdfbf6d99c84acd7",
      "deleted": "f",
      "create_datetime": "2025-01-05 13:35:19.638684+00",
      "create_datetime_usertime": "05 Jan 2025 14:35 CET",
      "thumbnail_url_huge": "https://nl1.ib.metapix.net/thumbnails/huge/5376/5376851_JustLurking_ib-maildir-fetch.jpg",
      "thumbnail_url_large": "https://nl1.ib.metapix.net/thumbnails/large/5376/5376851_JustLurking_ib-maildir-fetch.jpg",
      "thumbnail_url_medium": "https://nl1.ib.metapix.net/thumbnails/medium/5376/5376851_JustLurking_ib-maildir-fetch.jpg",
      "thumb_huge_x": "100",
      "thumb_huge_y": "100",
      "thumb_large_x": "100",
      "thumb_large_y": "100",
      "thumb_medium_x": "100",
      "thumb_medium_y": "100"
    }
  ],
  "pools": [
    {
      "pool_id": "3115",
      "name": "Everything in Order of Creation",
      "description": "Everything in my gallery in the order it was created.",
      "count": "70",
      "submission_left_submission_id": "3517765",
      "submission_left_file_name": "5376925_JustLurking_ib-status.txt",
      "submission_left_thumbnail_url_huge": "https://nl1.ib.metapix.net/thumbnails/huge/5376/5376925_JustLurking_ib-status.jpg",
      "submission_left_thumbnail_url_large": "https://nl1.ib.metapix.net/thumbnails/large/5376/5376925_JustLurking_ib-status.jpg",
      "submission_left_thumbnail_url_medium": "https://nl1.ib.metapix.net/thumbnails/medium/5376/5376925_JustLurking_ib-status.jpg",
      "submission_left_thumb_huge_x": "100",
      "submission_left_thumb_huge_y": "100",
      "submission_left_thumb_large_x": "100",
      "submission_left_thumb_large_y": "100",
      "submission_left_thumb_medium_x": "100",
      "submission_left_thumb_medium_y": "100",
      "submission_right_submission_id": "3606528",
      "submission_right_file_name": "5543647_JustLurking_vr_school_takeover.png",
      "submission_right_thumbnail_url_huge_noncustom": "https://nl1.ib.metapix.net/files/preview/5543/5543647_JustLurking_vr_school_takeover.jpg",
      "submission_right_thumbnail_url_large_noncustom": "https://nl1.ib.metapix.net/thumbnails/large/5543/5543647_JustLurking_vr_school_takeover_noncustom.jpg",
      "submission_right_thumbnail_url_medium_noncustom": "https://nl1.ib.metapix.net/thumbnails/medium/5543/5543647_JustLurking_vr_school_takeover_noncustom.jpg",
      "submission_right_thumb_medium_noncustom_x": "90",
      "submission_right_thumb_medium_noncustom_y": "120",
      "submission_right_thumb_large_noncustom_x": "150",
      "submission_right_thumb_large_noncustom_y": "200",
      "submission_right_thumb_huge_noncustom_x": "225",
      "submission_right_thumb_huge_noncustom_y": "300"
    },
    {
      "pool_id": "98445",
      "name": "Inkbunny Maildir Fetch",
      "description": "Software for fetching messages from Inkbunny and storing them locally in maildir format.",
      "count": "1"
    }
  ],
  "description": "A program which downloads new messages from Inkbunny and backs them up into a maildir on your local system.  The script attempts to to be frugal with its requests and only download new data from the site (so it doesn't have to trawl your entire inbox and sent items every time it is run).  It also attempts to convert Inkbunny's human-friendly dates back into time stamps, maintain threading information and undo BBCode rendering (the extracted, unmodified HTML is saved as the first alternative).\n\nBe careful what automation you setup with this.  If you fail to add generous sleeps/delays/waits between requests you will only piss off the admins and no one wants that.\n\nAlso be aware that: [b]unless told otherwise, this script will treat the current directory as the destination maildir[/b].  If this is your home or documents directory [b]you almost certainly do not want this[/b].  Create a new directory for this script and change to it before running this program.  [b]You have been warned[/b].\n\nThe intended usage is something like:\n\n[q]export PHPSESSID='my-session-id'\nmkdir -p ~/inkbuny-messages/\ncd ~/inkbunny-messages\nib-maildir-fetch[/q]\n\nMy Inkbunny Status Reporter script can be used in conjunction with this script to automatically download new messages (but be careful not to flood the site with requests in a tight loop; no one will appreciate that, the site admins least of all.)\n\nThe script doesn't do any login of its own because I felt trying to automate the login process would not endear me to the site admins.  I trust that users who want to run this script will know how to extract the session cookie from their browser.\n\nThere is no official API for fetching messages on inkbunny so this script may break in the future as the site is updated.  There's not much that can be done about that.\n\n[name]GreenReaper[/name], [name]Salmy[/name], [name]keito[/name], I believe these scripts fit in with the spirit of the other scripts found in the [url=Apps, Scripts and Mods]Apps, Scripts and Mods[/url] page and don't violate the ToS, please remove them if you disagree.  I will be happy to make any modifications you require to these scripts if there is something in them that concerns you.\n\n[center][i]This program is uploaded in the hopes that it may be useful to others.  No guarantee is made that this program is correct and free from faults.  Never run code on your system that you haven't first read and understood.[/i][/center]",
  "description_bbcode_parsed": "<span style='word-wrap: break-word;'>A program which downloads new messages from Inkbunny and backs them up into a maildir on your local system.&nbsp;&nbsp;The script attempts to to be frugal with its requests and only download new data from the site (so it doesn&#039;t have to trawl your entire inbox and sent items every time it is run).&nbsp;&nbsp;It also attempts to convert Inkbunny&#039;s human-friendly dates back into time stamps, maintain threading information and undo BBCode rendering (the extracted, unmodified HTML is saved as the first alternative).<br /><br />Be careful what automation you setup with this.&nbsp;&nbsp;If you fail to add generous sleeps/delays/waits between requests you will only piss off the admins and no one wants that.<br /><br />Also be aware that: <strong>unless told otherwise, this script will treat the current directory as the destination maildir</strong>.&nbsp;&nbsp;If this is your home or documents directory <strong>you almost certainly do not want this</strong>.&nbsp;&nbsp;Create a new directory for this script and change to it before running this program.&nbsp;&nbsp;<strong>You have been warned</strong>.<br /><br />The intended usage is something like:<br /><br />\n\t\t\t\t\t<div class='bbcode_quote'>\n\t\t\t\t\t\t<table cellpadding='0' cellspacing='0'>\n\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t<td class='bbcode_quote_symbol' rowspan='2'>&quot;</td>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t<td class='bbcode_quote_quote'>\n\t\t\t\t\t\t\t\t\texport PHPSESSID=&#039;my-session-id&#039;<br />mkdir -p ~/inkbuny-messages/<br />cd ~/inkbunny-messages<br />ib-maildir-fetch\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</div>\n\t\t\t\t\t<br /><br />My Inkbunny Status Reporter script can be used in conjunction with this script to automatically download new messages (but be careful not to flood the site with requests in a tight loop; no one will appreciate that, the site admins least of all.)<br /><br />The script doesn&#039;t do any login of its own because I felt trying to automate the login process would not endear me to the site admins.&nbsp;&nbsp;I trust that users who want to run this script will know how to extract the session cookie from their browser.<br /><br />There is no official API for fetching messages on inkbunny so this script may break in the future as the site is updated.&nbsp;&nbsp;There&#039;s not much that can be done about that.<br /><br /><span class=\"widget_userNameSmall \"><a class=\"widget_userNameSmall\" href=\"/GreenReaper\">GreenReaper</a></span>, <span class=\"widget_userNameSmall \"><a class=\"widget_userNameSmall\" href=\"/Salmy\">Salmy</a></span>, <span class=\"widget_userNameSmall \"><a class=\"widget_userNameSmall\" href=\"/keito\">keito</a></span>, I believe these scripts fit in with the spirit of the other scripts found in the <a href=\"http://Apps, Scripts and Mods\" rel=\"nofollow\">Apps, Scripts and Mods</a> page and don&#039;t violate the ToS, please remove them if you disagree.&nbsp;&nbsp;I will be happy to make any modifications you require to these scripts if there is something in them that concerns you.<br /><br /><div class='align_center'><em>This program is uploaded in the hopes that it may be useful to others.&nbsp;&nbsp;No guarantee is made that this program is correct and free from faults.&nbsp;&nbsp;Never run code on your system that you haven&#039;t first read and understood.</em></div></span>",
  "writing": "[code]#!/usr/bin/python3\n\n# Inkbunny Maildir Fetch 0.1.0\n# Copyright 2025 JustLurking\n\n# This program is free software: you can redistribute it and/or modify it\n# under the terms of the GNU General Public License as published by the\n# Free Software Foundation, either version 3 of the License, or (at your\n# option) any later version.\n#\n# This program is distributed in the hope that it will be useful, but\n# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License\n# for more details.\n#\n# You should have received a copy of the GNU General Public License along\n# with this program. If not, see <https://www.gnu.org/licenses/>.\n\n# About\n# This program will connect to Inkbunny using the session cookie in the\n# PHPSESSID environment variable then download the messages in the user's\n# inbox and sent items, storing them as emails in maildir format in the\n# current working directory.\n#\n# As the program treats the current working directory as a maildir mailbox\n# it is important to change to the correct directory before running it.\n#\n# Only new items will be downloaded and saved.\n#\n# The program does its best to convert the HTML of a message back to BBCode,\n# but this is an imperfect conversion and the original HTML is saved also\n# so that either maybe used.\n#\n# Similarly, it is not possible to convert timestamps such as `a moment ago`\n# or `1 hrs, 20 mins ago` to the correct dates and times with full accuracy\n# the program does its best in those cases but some messages may be saved\n# with dates different to those shown on the site.\n\n# Changelog\n# 2025-01-03 JustLurking: Initial Release.\n\nimport argparse\nimport bs4\nimport dataclasses\nimport email\nimport logging\nimport mailbox\nimport os\nimport re\nimport requests\nimport time\nimport urllib.parse\n\n# Variables used throughout the program.\n\n# Used for identifying the program.\nprogram_file  = \"ib-maildir-fetch\"\nprogram_name  = \"Inkbunny Maildir Fetch\"\nversion       = \"0.1.0\"\nbug_reports   = \"https://Inkbunny.net/JustLurking/\"\nhomepage      = \"https://inkbunny.net/submissionsviewall.php?mode=pool&pool_id=98445\"\n\n# Used to download pages.\nbase_url      = \"https://inkbunny.net/privatemessages.php\"\ncookie        = None\ncookies       = requests.cookies.RequestsCookieJar()\n\n# Used for rate limiting.\nrequest_pause = 0.25\nlast_request  = 0\n\n# Used to hold site details.\nuser          = None\ndownloaded    = set()\nmaildir       = None\ninbox         = None\nsent          = None\nlatest_sent   = 0\nlatest_inbox  = 0\nearly_quit    = True\n\n# Used for logging.\nlog           = logging.getLogger(__name__)\n\n# Variables used when converting HTML to BBCode.\n\n# These replacements are all simple substitutions.\nsimple_replacements = {\n    \"b\":        ((\"strong\",),   {}),\n    \"center\":   ((\"div\",),      {\"class_\": \"align_center\"}),\n    \"i\":        ((\"em\",),       {}),\n    \"left\":     ((\"div\",),      {\"class_\": \"align_left\"}),\n    \"right\":    ((\"div\",),      {\"class_\": \"align_right\"}),\n    \"s\":        ((\"span\",),     {\"class_\": \"strikethrough\"}),\n    \"t\":        ((\"span\",),     {\"class_\": \"font_title\"}),\n    \"u\":        ((\"span\",),     {\"class_\": \"underline\"})\n}\n\n# Map titles back to tag names.\noffsite_mapping = {\n    \"deviantART\": \"da\",\n    \"Fur Affinity\": \"fa\",\n    \"SoFurry\": \"sf\",\n    \"Weasyl\": \"w\"\n}\n\n# Regular expressions.\nre_offsite_title = re.compile(\" on (deviantART|Fur Affinity|SoFurry|Weasyl)$\")\nre_usericon      = re.compile(\"^https://inkbunny.net/usericons/\")\nre_color         = re.compile(\"^color: [^;]*;$\")\nre_size          = re.compile(\"/(small|medium|large|huge)/\")\nre_pool_url      = re.compile(\"^/poolview_process.php\\\\?pool_id=\")\nre_bare          = re.compile(\"\\\\[(b|center|i|left|right|s|t|u|q|smallpool|mediumpool|smallthumb|mediumthumb|largethumb|hugethumb|color|icon|iconname|name|da|fa|sf|w|url)(=[^]]*)?]|da!|fa!|sf!|w!\")\nre_date          = re.compile(\"^((\\\\d+) hrs?,? )?((\\\\d+) mins?,? )?((\\\\d+) secs? )?ago$\")\n\n# Classes and functions.\n\n[at_iconname]dataclasses[/at_iconname].dataclass\nclass MessageMetadata:\n    \"\"\"\n    Class that holds the metadata identifying a specific Inkbunny message.\n    \"\"\"\n    msg_id: int\n    sender: str\n    receiver: str\n    subject: str\n    date: str\n    in_reply_to: str\n\n    def download_messages_in_thread(self):\n        \"\"\"\n        Fetch the thread for this message if it has not been downloaded yet\n        and extracts all messages from it, saving them to the correct folder\n        if they haven't been downloaded already.\n        \"\"\"\n        global user, downloaded\n\n        if int(self.msg_id) in downloaded:\n            log.debug(\"Ignoring thread for message %d, message already saved.\", self.msg_id)\n            return\n\n        log.debug(\"Fetching thread for message %d.\", self.msg_id)\n\n        soup = download(\n                \"https://inkbunny.net/privatemessageview.php\",\n                private_message_id=str(self.msg_id)\n            )\n        get_logged_in_user(soup)\n        other_user = self.sender if self.sender != user else self.receiver\n\n        for msg in read_thread(soup, other_user):\n            msg_id = int(msg[\"Message-Id\"])\n            if msg_id in downloaded:\n                log.debug(\"Ignoring message %d in thread %d, message already saved.\", msg_id, self.msg_id)\n                continue\n\n            if msg[\"To\"] == user:\n                log.debug(\"Saving message %d in thread %d to inbox.\", msg_id, self.msg_id)\n                inbox.add(msg)\n            else:\n                log.debug(\"Saving message %d in thread %d to sent.\", msg_id, self.msg_id)\n                sent.add(msg)\n            downloaded.add(msg_id)\n\n    def create_message(self):\n        \"\"\"\n        Create a new multi-part email.message.EmailMessage with the headers\n        set from this metadata object.  Does not add a body, that is up to\n        the caller.\n        \"\"\"\n        msg = email.message.EmailMessage()\n        msg[\"Message-Id\"] = str(self.msg_id)\n        msg[\"From\"]       = self.sender\n        msg[\"To\"]         = self.receiver\n        msg[\"Date\"]       = self.date\n        msg[\"Subject\"]    = self.subject\n        if self.in_reply_to is not None:\n            msg[\"In-Reply-To\"] = str(self.in_reply_to)\n        msg.make_alternative()\n        return msg\n\ndef download(url, **kwargs):\n    \"\"\"\n    Utility wrapper for boiler plate around downloading and parsing pages.\n    Also enforces the rate limit.\n    \"\"\"\n    global cookies, last_request, request_pause\n    now = time.time()\n    if now - last_request < request_pause:\n        time.sleep(request_pause - now + last_request)\n    resp = requests.get(url, cookies=cookies, params=kwargs)\n    for (i, step) in enumerate(resp.history):\n        log.debug(\"[%d] Downloaded: %s\", i, step.url)\n    log.debug(\"[Final] Downloaded: %s\", resp.url)\n    resp.raise_for_status()\n    last_request = time.time()\n    return bs4.BeautifulSoup(resp.content, features=\"lxml\")\n\ndef parse_date(text):\n    \"\"\"\n    Convert a human-readable time to something that can be stored in an\n    email header.\n    \"\"\"\n    global re_date, last_request\n\n    to_subtract = None\n    if text == \"a moment ago\":\n        to_subtract = 0\n\n    m = re_date.match(text)\n    if m is not None:\n        to_subtract = 0\n        if m.group(2) is not None:\n            to_subtract += int(m.group(2)) * 60 * 60\n        if m.group(4) is not None:\n            to_subtract += int(m.group(4)) * 60\n        if m.group(6) is not None:\n            to_subtract += int(m.group(6))\n\n    if to_subtract is not None:\n        result = time.strftime(\"%a, %d %b %Y %H:%M:%S +0000\", time.gmtime(last_request - to_subtract))\n        return result\n\n    return text\n\ndef get_logged_in_user(tag):\n    \"\"\"\n    Sets the logged-in user name if it has not been set yet using the\n    supplied HTML.\n    \"\"\"\n    global user\n    if user is not None:\n        return\n    nav = tag.find(class_=\"userdetailsnavigation\")\n    if nav is None:\n        log.critical(\"Unable to find user details on page.\")\n        exit(1)\n    widget = nav.find(class_=\"widget_userNameSmall\")\n    if widget is None:\n        log.critical(\"Unable to find user details on page.\")\n        exit(1)\n    user = widget.get_text().strip()\n    log.info(\"Logged in as %s.\", user)\n\ndef get_next_page(tag):\n    \"\"\"\n    Returns the URL of the first next-page link in the supplied HTML.\n    \"\"\"\n    next_page_link = tag.find(\"a\", title=\"next page\")\n    if next_page_link is None:\n        return None\n    return next_page_link[\"href\"]\n\ndef read_box_page(tag, in_inbox):\n    \"\"\"\n    A generator which extracts rows from the index view of a box.\n    If is_inbox is True operates over the inbox, otherwise it operates\n    over the sent items.\n    \"\"\"\n    global user\n    get_logged_in_user(tag)\n\n    # Set the variables for the box we will be reading.\n    min_columns    = 5\n    user_column    = 1\n    subject_column = 3\n    date_column    = 4\n\n    if in_inbox:\n        min_columns    = 6\n        user_column    = 2\n        subject_column = 4\n        date_column    = 5\n\n    # Read the rows from this page of the index.\n    for row in tag.find_all(id=re.compile(\"^m_\\\\d+$\")):\n        columns = [*row.find_all(\"td\", recursive=False)]\n        if len(columns) < min_columns:\n            # Malformed row?  Skip it.\n            continue\n\n        other_user = columns[user_column].get_text().strip()\n        subject    = columns[subject_column].get_text().strip()\n        date       = parse_date(columns[date_column].get_text().strip())\n        msg_id     = int(row[\"id\"][2:])\n\n        if in_inbox:\n            yield MessageMetadata(msg_id, other_user, user, subject, date, None)\n        else:\n            yield MessageMetadata(msg_id, user, other_user, subject, date, None)\n\ndef read_box(is_inbox):\n    \"\"\"\n    A generator which extracts rows from a box.\n    If is_inbox is True operates over the inbox, otherwise it operates\n    over the sent items.\n    \"\"\"\n    global latest_inbox, latest_sent, early_quit\n\n    # Set the variables for the box we will be reading.\n    latest = latest_inbox if is_inbox else latest_sent\n    end_of_new_messages = False\n\n    next_page = \"https://inkbunny.net/privatemessages_process.php?mode=\"\n    if is_inbox:\n        log.info(\"Reading Inbox.\")\n        next_page += \"inbox\"\n    else:\n        log.info(\"Reading Sent.\")\n        next_page += \"sent\"\n\n    # Download and parse the next page of the index.\n    while next_page is not None:\n        next_page = urllib.parse.urljoin(base_url, next_page)\n        soup = download(next_page)\n\n        # Read the rows in the index and yield them to the caller.\n        for row in read_box_page(soup, is_inbox):\n            if row.msg_id <= latest:\n                end_of_new_messages = True\n            yield row\n\n        # Exit the loop early if we've encountered a message on this page\n        # older than one we've already downloaded.\n        if early_quit and end_of_new_messages:\n            log.info(\"End of new messages in this box.\")\n            break\n\n        next_page = get_next_page(soup)\n\ndef unparse_html_to_bbcode(tag):\n    \"\"\"\n    Given some HTML generated by Inkbunny's BBCode renderer attempt to return\n    the BBCode which might have generated it.  Since the transformation is\n    non-injective it's impossible to reverse with 100% accuracy.\n    White-space will likely not be preserved either.\n    \"\"\"\n    global simple_replacements, offsite_mapping, re_offsite_title, re_usericon, re_color, re_size, re_pool_url, re_bare\n\n    # [code]\n    for string in tag.find_all(string=re_bare):\n        string.replace_with(re_bare.sub(lambda m: \"[code]\"+m.group(0)+\"[​/code]\", string.string))\n\n    # [q] and [q=someone]\n    for quote in tag.find_all(class_=\"bbcode_quote\"):\n        author = quote.find(class_=\"bbcode_quote_author\")\n        body = quote.find(class_=\"bbcode_quote_quote\").extract()\n        argument = \"\"\n        if author is not None:\n            argument = \"=\" + author.get_text().strip()[:-7]\n        quote.insert_before(bs4.NavigableString(\"[q\"+argument+\"]\"))\n        quote.insert_before(body)\n\n        children = (*body.contents,)\n        if len(children) == 1:\n            if isinstance(children[0], bs4.NavigableString):\n                children[0].replace_with(children[0].string.strip())\n        elif len(children) > 1:\n            if isinstance(children[0], bs4.NavigableString):\n                children[0].replace_with(children[0].string.lstrip())\n            if isinstance(children[-1], bs4.NavigableString):\n                children[-1].replace_with(children[-1].string.rstrip())\n\n        body.unwrap()\n        quote.insert_after(bs4.NavigableString(\"[/q]\"))\n        quote.extract()\n\n    # [smallpool], [mediumpool]\n    for pool in tag.find_all(class_=\"widget_imageFromSubmission\"):\n        table = pool.find_parent(\"table\")\n        if table is None:\n            continue\n        table = table.find_parent(\"table\")\n        if table is None or table.parent is None:\n            # There are three divs with widget_imageFromSubmissions as their\n            # class per pool table, so it is possible we've already processed\n            # this pool.\n            # This also filters out thumbnails which get processed after.\n            continue\n        size = re_size.search(pool.find(\"img\")[\"src\"]).group(1)\n        pool_id = table.find(\"a\", href=re_pool_url)[\"href\"][31:]\n        table.replace_with(bs4.NavigableString(\"[\"+size+\"pool]\"+pool_id+\"[/\"+size+\"pool]\"))\n\n    # [smallthumb], [mediumthumb], [largethumb], [hugethumb]\n    for thumb in tag.find_all(class_=\"widget_imageFromSubmission\"):\n        table = thumb.find_parent(\"table\")\n        size = re_size.search(thumb.find(\"img\")[\"src\"]).group(1)\n        submission_id = thumb.find(\"a\")[\"href\"][3:]\n        table.replace_with(bs4.NavigableString(\"[\"+size+\"thumb]\"+submission_id+\"[/\"+size+\"thumb]\"))\n\n    # [color]\n    for color in tag.find_all(\"span\", style=re_color):\n        color.insert_before(bs4.NavigableString(\"[color=\"+color[\"style\"][7:-1]+\"]\"))\n        color.insert_after(bs4.NavigableString(\"[/color]\"))\n        color.unwrap()\n\n    # Newlines\n    for br in tag.find_all(\"br\"):\n        br.replace_with(bs4.NavigableString(\"\\n\"))\n\n    # [icon], [iconname]\n    for icon in tag.find_all(\"img\", src=re_usericon):\n        table = icon.find_parent(\"table\")\n        link = icon.find_parent(\"a\")\n        name = link[\"href\"][21:]\n        if len((*table.find_all(\"a\"),)) > 1:\n            table.replace_with(\"[iconname]\"+name+\"[/iconname]\")\n        else:\n            table.replace_with(\"[icon]\"+name+\"[/icon]\")\n\n    # [name]\n    for namelink in tag.find_all(\"span\", class_=\"widget_userNameSmall\"):\n        name = namelink.find(\"a\")[\"href\"][1:]\n        namelink.replace_with(\"[name]\"+name+\"[/name]\")\n\n    # [da], [fa], [sf], [w]\n    for link in tag.find_all(\"a\", title=re_offsite_title):\n        # Each off-site link generates two sibling elements in the output, we\n        # can replace either, but we should remove the one we don't replace.\n        if link.find(\"img\") is None:\n            # Replace the textual link.\n            m = re_offsite_title.search(link[\"title\"])\n            site = offsite_mapping[m.group(1)]\n            link.replace_with(\"[\"+site+\"]\"+link.get_text().strip()+\"[/\"+site+\"]\")\n        else:\n            # Remove the image link.\n            link.extract()\n\n    # [url]\n    for a in tag.find_all(\"a\"):\n        a.insert_before(bs4.NavigableString(\"[url=\"+a[\"href\"]+\"]\"))\n        a.insert_after(bs4.NavigableString(\"[/url]\"))\n        a.unwrap()\n\n    # simple replacements\n    for (name, args) in simple_replacements.items():\n        for target in tag.find_all(*args[0], **args[1]):\n            target.insert_before(bs4.NavigableString(\"[\"+name+\"]\"))\n            target.insert_after(bs4.NavigableString(\"[/\"+name+\"]\"))\n            target.unwrap()\n\n    # combine all text nodes and return the string representation of the child\n    # nodes.\n    tag.smooth()\n    return \"\".join(str(n) for n in tag.contents)\n\ndef read_thread(tag, other_user):\n    \"\"\"\n    A generator which extracts messages from thread view.\n    \"\"\"\n    global user, messages\n    previous_subject = None\n    previous_msg_id  = None\n\n    got_result = False\n    for elem in tag.find_all(id=re.compile(\"^irt_message_\\\\d+$\")):\n        # Extract the next message's data from the page.\n        msg_id = int(elem[\"id\"][12:])\n\n        children = [e for e in elem.children if not isinstance(e, bs4.NavigableString)]\n\n        if len(children) < 5:\n            # Malformed\n            continue\n\n        date_field    = children[2].find(True)\n        body_span     = children[2].find(\"span\", style=\"word-wrap: break-word;\")\n\n        if date_field is None or body_span is None:\n            # Malformed\n            continue\n\n        subject_div = children[2].find(style=\"margin-bottom: 5px;\")\n        left_link   = children[0].find(class_=\"widget_userNameSmall\")\n        right_link  = children[4].find(class_=\"widget_userNameSmall\")\n\n        date        = parse_date(date_field.get_text().strip())\n        subject     = subject_div.get_text().strip() if subject_div is not None else previous_subject\n        in_reply_to = previous_msg_id\n        sender      = None\n\n        if left_link is not None:\n            sender = left_link.get_text().strip()\n        elif right_link is not None:\n            sender = right_link.get_text().strip()\n        else:\n            continue\n\n        receiver = other_user if sender == user else user\n\n        # Create, populate and yield an EmailMessage for this message.\n        content = bs4.BeautifulSoup(\"<html><title/><body/>\", features=\"lxml\")\n        content.title.string = subject\n        content.body.append(body_span)\n        body_span.unwrap()\n\n        metadata = MessageMetadata(msg_id, sender, receiver, subject, date, in_reply_to)\n        msg = metadata.create_message()\n        msg.add_alternative(\"<!DOCTYPE html>\" + str(content), \"html\")\n        msg.add_alternative(str(unparse_html_to_bbcode(content.body)))\n        yield msg\n        got_result = True\n\n        # Update information on the previous message (the one we just yielded)\n        # prior to extracting next message.\n        if subject is not None:\n            previous_subject = subject\n        previous_msg_id = msg_id\n\n    # This should only be reached if a page other than the one we were\n    # expecting is returned, i.e. an error page or similar.\n    # Since something has clearly gone wrong just inform the user and abort.\n    if not got_result:\n        log.critical(\"Failed to parse at least one message in thread.  Got: %s\", str(tag))\n        exit(1)\n\n# Main Program starts here.\n\n# Configure logging.\nlog.addHandler(logging.StreamHandler())\nlog.setLevel(logging.INFO)\n\n# Handle arguments.\narg_parser = argparse.ArgumentParser(\n    prog = program_file,\n    description = \"\".join((\n        \"Downloads messages from the Inkbunny.net website and stores them\",\n        \" as emails in maildir format.\"\n    )),\n    epilog = \"\".join((\n        \"If no DIRECTORY is specified then the current directory is\",\n        \" assumed to be the maildir, care should be taken to ensure that\",\n        \" the program is run in the correct directory or it may read or\",\n        \" write to files the user does not want it to.\\n\",\n        \"\\n\",\n        \"The SESSION may be passed using the PHPSESSID environment variable.\"\n        \" this may be useful if you wish to pass the same value to multiple\"\n        \" programs, or if you wish to set the value in your shell's profile.\\n\"\n        \"\\n\",\n        \"Report bugs to: \"+bug_reports+\"\\n\",\n        program_name + \" home page: <\"+homepage+\"\\n\"\n    )),\n    formatter_class = argparse.RawDescriptionHelpFormatter\n)\narg_parser.add_argument(\n    \"-s\",\n    \"--session\",\n    nargs = 1,\n    help = \"the session id to send with requests\"\n)\narg_parser.add_argument(\n    \"-C\",\n    \"--directory\",\n    nargs = 1,\n    help = \"change directory when program starts\"\n)\narg_parser.add_argument(\n    \"-q\",\n    \"--quiet\",\n    action = \"count\",\n    default = 2,\n    help = \"decrease verbosity\"\n)\narg_parser.add_argument(\n    \"-v\",\n    \"--verbose\",\n    action = \"count\",\n    default = 0,\n    help = \"increase verbosity\"\n)\narg_parser.add_argument(\n    \"-f\",\n    \"--full-scan\",\n    action = \"store_true\",\n    help = \"read the full message index even if it seems unnecessary\"\n)\narg_parser.add_argument(\n    \"-V\",\n    \"--version\",\n    action = \"store_true\"\n)\n\nargs = arg_parser.parse_args()\n\nif args.version:\n    print(\n        \" \".join((program_name, version)),\n        \"Copyright (C) 2025 JustLurking<https://inkbunny.net/JustLurking>\",\n        \"License GPLv3+: GNU GPL version 3 or later \"+\n            \"<https://gnu.org/licenses/gpl.html>\",\n        \"\",\n        \"This is free software: you are free to change and redistribute it.\",\n        \"There is NO WARRANTY, to the extent permitted by law.\",\n        sep=\"\\n\"\n    )\n    exit(0)\n\nlog.setLevel(max(1, min(5, args.quiet-args.verbose))*10)\n\nif args.directory is not None:\n    log.info(\"Changing to directory %d.\")\n    os.chdir(args.directory)\n\nif args.full_scan:\n    early_quit = False\n\nif args.session is not None:\n    cookie = args.session\n\n# Open or create Maildir.\nmaildir = mailbox.Maildir(\".\")\ninbox   = maildir.add_folder(\"inbox\")\nsent    = maildir.add_folder(\"sent\")\n\n# Set the Session Cookie from the environment.\nif cookie is None:\n    cookie = os.getenv(\"PHPSESSID\")\n\nif cookie is None:\n    log.error(\"Please use the -s argument or set the PHPSESSID environment variable to set the session id.\")\n    exit(1)\ncookies.set(\"PHPSESSID\", cookie, domain=\"inkbunny.net\", path=\"/\")\n\n# Make a list of all already downloaded messages.\nfor msg in inbox:\n        msg_id = int(msg[\"Message-Id\"])\n        downloaded.add(msg_id)\n        latest_inbox = max(latest_inbox, msg_id)\n\nfor msg in sent:\n        msg_id = int(msg[\"Message-Id\"])\n        downloaded.add(msg_id)\n        latest_sent = max(latest_sent, msg_id)\n\n# Fetch message indices for inbox and sent and for each message found fetch the\n# appropriate thread view and download all new messages in that thread.\nfor row in read_box(True):\n    row.download_messages_in_thread()\n\nfor row in read_box(False):\n    row.download_messages_in_thread()\n[/code]",
  "writing_bbcode_parsed": "<span style='word-wrap: break-word;'>#!/usr/bin/python3<br /><br /># Inkbunny Maildir Fetch 0.1.0<br /># Copyright 2025 JustLurking<br /><br /># This program is free software: you can redistribute it and/or modify it<br /># under the terms of the GNU General Public License as published by the<br /># Free Software Foundation, either version 3 of the License, or (at your<br /># option) any later version.<br />#<br /># This program is distributed in the hope that it will be useful, but<br /># WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY<br /># or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License<br /># for more details.<br />#<br /># You should have received a copy of the GNU General Public License along<br /># with this program. If not, see &lt;<a href=\"https://www.gnu.org/licenses/&gt;\" rel=\"nofollow\">https://www.gnu.org/licenses/&gt;</a>.<br /><br /># About<br /># This program will connect to Inkbunny using the session cookie in the<br /># PHPSESSID environment variable then download the messages in the user&#039;s<br /># inbox and sent items, storing them as emails in maildir format in the<br /># current working directory.<br />#<br /># As the program treats the current working directory as a maildir mailbox<br /># it is important to change to the correct directory before running it.<br />#<br /># Only new items will be downloaded and saved.<br />#<br /># The program does its best to convert the HTML of a message back to BBCode,<br /># but this is an imperfect conversion and the original HTML is saved also<br /># so that either maybe used.<br />#<br /># Similarly, it is not possible to convert timestamps such as `a moment ago`<br /># or `1 hrs, 20 mins ago` to the correct dates and times with full accuracy<br /># the program does its best in those cases but some messages may be saved<br /># with dates different to those shown on the site.<br /><br /># Changelog<br /># 2025-01-03 JustLurking: Initial Release.<br /><br />import argparse<br />import bs4<br />import dataclasses<br />import email<br />import logging<br />import mailbox<br />import os<br />import re<br />import requests<br />import time<br />import urllib.parse<br /><br /># Variables used throughout the program.<br /><br /># Used for identifying the program.<br />program_file&nbsp;&nbsp;= &quot;ib-maildir-fetch&quot;<br />program_name&nbsp;&nbsp;= &quot;Inkbunny Maildir Fetch&quot;<br />version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = &quot;0.1.0&quot;<br />bug_reports&nbsp;&nbsp; = &quot;<a href=\"https://Inkbunny.net/JustLurking/&quot\" rel=\"nofollow\">https://Inkbunny.net/JustLurking/&quot</a>;<br />homepage&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= &quot;<a href=\"https://inkbunny.net/submissionsviewall.php?mode=pool&amp;pool_id=98445&quot\" rel=\"nofollow\">https://inkbunny.net/submissionsviewall.php?mode=pool&a...</a>;<br /><br /># Used to download pages.<br />base_url&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= &quot;<a href=\"https://inkbunny.net/privatemessages.php&quot\" rel=\"nofollow\">https://inkbunny.net/privatemessages.php&quot</a>;<br />cookie&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= None<br />cookies&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = requests.cookies.RequestsCookieJar()<br /><br /># Used for rate limiting.<br />request_pause = 0.25<br />last_request&nbsp;&nbsp;= 0<br /><br /># Used to hold site details.<br />user&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= None<br />downloaded&nbsp;&nbsp;&nbsp;&nbsp;= set()<br />maildir&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = None<br />inbox&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = None<br />sent&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= None<br />latest_sent&nbsp;&nbsp; = 0<br />latest_inbox&nbsp;&nbsp;= 0<br />early_quit&nbsp;&nbsp;&nbsp;&nbsp;= True<br /><br /># Used for logging.<br />log&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = logging.getLogger(__name__)<br /><br /># Variables used when converting HTML to BBCode.<br /><br /># These replacements are all simple substitutions.<br />simple_replacements = {<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;b&quot;:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;((&quot;strong&quot;,),&nbsp;&nbsp; {}),<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;center&quot;:&nbsp;&nbsp; ((&quot;div&quot;,),&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&quot;class_&quot;: &quot;align_center&quot;}),<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;i&quot;:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;((&quot;em&quot;,),&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {}),<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;left&quot;:&nbsp;&nbsp;&nbsp;&nbsp; ((&quot;div&quot;,),&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&quot;class_&quot;: &quot;align_left&quot;}),<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;right&quot;:&nbsp;&nbsp;&nbsp;&nbsp;((&quot;div&quot;,),&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&quot;class_&quot;: &quot;align_right&quot;}),<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;s&quot;:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;((&quot;span&quot;,),&nbsp;&nbsp;&nbsp;&nbsp; {&quot;class_&quot;: &quot;strikethrough&quot;}),<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;t&quot;:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;((&quot;span&quot;,),&nbsp;&nbsp;&nbsp;&nbsp; {&quot;class_&quot;: &quot;font_title&quot;}),<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;u&quot;:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;((&quot;span&quot;,),&nbsp;&nbsp;&nbsp;&nbsp; {&quot;class_&quot;: &quot;underline&quot;})<br />}<br /><br /># Map titles back to tag names.<br />offsite_mapping = {<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;deviantART&quot;: &quot;da&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;Fur Affinity&quot;: &quot;fa&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;SoFurry&quot;: &quot;sf&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;Weasyl&quot;: &quot;w&quot;<br />}<br /><br /># Regular expressions.<br />re_offsite_title = re.compile(&quot; on (deviantART|Fur Affinity|SoFurry|Weasyl)$&quot;)<br />re_usericon&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= re.compile(&quot;^<a href=\"https://inkbunny.net/usericons/&quot;\" rel=\"nofollow\">https://inkbunny.net/usericons/&quot;</a>)<br />re_color&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = re.compile(&quot;^color: [^;]*;$&quot;)<br />re_size&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= re.compile(&quot;/(small|medium|large|huge)/&quot;)<br />re_pool_url&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= re.compile(&quot;^/poolview_process.php\\\\?pool_id=&quot;)<br />re_bare&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= re.compile(&quot;\\\\[(b|center|i|left|right|s|t|u|q|smallpool|mediumpool|smallthumb|mediumthumb|largethumb|hugethumb|color|icon|iconname|name|da|fa|sf|w|url)(=[^]]*)?]|da!|fa!|sf!|w!&quot;)<br />re_date&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= re.compile(&quot;^((\\\\d+) hrs?,? )?((\\\\d+) mins?,? )?((\\\\d+) secs? )?ago$&quot;)<br /><br /># Classes and functions.<br /><br />[at_iconname]dataclasses[/at_iconname].dataclass<br />class MessageMetadata:<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Class that holds the metadata identifying a specific Inkbunny message.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;msg_id: int<br />&nbsp;&nbsp;&nbsp;&nbsp;sender: str<br />&nbsp;&nbsp;&nbsp;&nbsp;receiver: str<br />&nbsp;&nbsp;&nbsp;&nbsp;subject: str<br />&nbsp;&nbsp;&nbsp;&nbsp;date: str<br />&nbsp;&nbsp;&nbsp;&nbsp;in_reply_to: str<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;def download_messages_in_thread(self):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Fetch the thread for this message if it has not been downloaded yet<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;and extracts all messages from it, saving them to the correct folder<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if they haven&#039;t been downloaded already.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;global user, downloaded<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if int(self.msg_id) in downloaded:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.debug(&quot;Ignoring thread for message %d, message already saved.&quot;, self.msg_id)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.debug(&quot;Fetching thread for message %d.&quot;, self.msg_id)<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;soup = download(<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;<a href=\"https://inkbunny.net/privatemessageview.php&quot;\" rel=\"nofollow\">https://inkbunny.net/privatemessageview.php&quot;</a>,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;private_message_id=str(self.msg_id)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;get_logged_in_user(soup)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;other_user = self.sender if self.sender != user else self.receiver<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for msg in read_thread(soup, other_user):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg_id = int(msg[&quot;Message-Id&quot;])<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if msg_id in downloaded:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.debug(&quot;Ignoring message %d in thread %d, message already saved.&quot;, msg_id, self.msg_id)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if msg[&quot;To&quot;] == user:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.debug(&quot;Saving message %d in thread %d to inbox.&quot;, msg_id, self.msg_id)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;inbox.add(msg)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.debug(&quot;Saving message %d in thread %d to sent.&quot;, msg_id, self.msg_id)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sent.add(msg)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;downloaded.add(msg_id)<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;def create_message(self):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Create a new multi-part email.message.EmailMessage with the headers<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;set from this metadata object.&nbsp;&nbsp;Does not add a body, that is up to<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;the caller.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg = email.message.EmailMessage()<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg[&quot;Message-Id&quot;] = str(self.msg_id)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg[&quot;From&quot;]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = self.sender<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg[&quot;To&quot;]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = self.receiver<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg[&quot;Date&quot;]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = self.date<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg[&quot;Subject&quot;]&nbsp;&nbsp;&nbsp;&nbsp;= self.subject<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if self.in_reply_to is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg[&quot;In-Reply-To&quot;] = str(self.in_reply_to)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg.make_alternative()<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return msg<br /><br />def download(url, **kwargs):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Utility wrapper for boiler plate around downloading and parsing pages.<br />&nbsp;&nbsp;&nbsp;&nbsp;Also enforces the rate limit.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;global cookies, last_request, request_pause<br />&nbsp;&nbsp;&nbsp;&nbsp;now = time.time()<br />&nbsp;&nbsp;&nbsp;&nbsp;if now - last_request &lt; request_pause:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;time.sleep(request_pause - now + last_request)<br />&nbsp;&nbsp;&nbsp;&nbsp;resp = requests.get(url, cookies=cookies, params=kwargs)<br />&nbsp;&nbsp;&nbsp;&nbsp;for (i, step) in enumerate(resp.history):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.debug(&quot;[%d] Downloaded: %s&quot;, i, step.url)<br />&nbsp;&nbsp;&nbsp;&nbsp;log.debug(&quot;[Final] Downloaded: %s&quot;, resp.url)<br />&nbsp;&nbsp;&nbsp;&nbsp;resp.raise_for_status()<br />&nbsp;&nbsp;&nbsp;&nbsp;last_request = time.time()<br />&nbsp;&nbsp;&nbsp;&nbsp;return bs4.BeautifulSoup(resp.content, features=&quot;lxml&quot;)<br /><br />def parse_date(text):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Convert a human-readable time to something that can be stored in an<br />&nbsp;&nbsp;&nbsp;&nbsp;email header.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;global re_date, last_request<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;to_subtract = None<br />&nbsp;&nbsp;&nbsp;&nbsp;if text == &quot;a moment ago&quot;:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;to_subtract = 0<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;m = re_date.match(text)<br />&nbsp;&nbsp;&nbsp;&nbsp;if m is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;to_subtract = 0<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if m.group(2) is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;to_subtract += int(m.group(2)) * 60 * 60<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if m.group(4) is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;to_subtract += int(m.group(4)) * 60<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if m.group(6) is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;to_subtract += int(m.group(6))<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;if to_subtract is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result = time.strftime(&quot;%a, %d %b %Y %H:%M:%S +0000&quot;, time.gmtime(last_request - to_subtract))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return result<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;return text<br /><br />def get_logged_in_user(tag):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Sets the logged-in user name if it has not been set yet using the<br />&nbsp;&nbsp;&nbsp;&nbsp;supplied HTML.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;global user<br />&nbsp;&nbsp;&nbsp;&nbsp;if user is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return<br />&nbsp;&nbsp;&nbsp;&nbsp;nav = tag.find(class_=&quot;userdetailsnavigation&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;if nav is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.critical(&quot;Unable to find user details on page.&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit(1)<br />&nbsp;&nbsp;&nbsp;&nbsp;widget = nav.find(class_=&quot;widget_userNameSmall&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;if widget is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.critical(&quot;Unable to find user details on page.&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit(1)<br />&nbsp;&nbsp;&nbsp;&nbsp;user = widget.get_text().strip()<br />&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;Logged in as %s.&quot;, user)<br /><br />def get_next_page(tag):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Returns the URL of the first next-page link in the supplied HTML.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;next_page_link = tag.find(&quot;a&quot;, title=&quot;next page&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;if next_page_link is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return None<br />&nbsp;&nbsp;&nbsp;&nbsp;return next_page_link[&quot;href&quot;]<br /><br />def read_box_page(tag, in_inbox):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;A generator which extracts rows from the index view of a box.<br />&nbsp;&nbsp;&nbsp;&nbsp;If is_inbox is True operates over the inbox, otherwise it operates<br />&nbsp;&nbsp;&nbsp;&nbsp;over the sent items.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;global user<br />&nbsp;&nbsp;&nbsp;&nbsp;get_logged_in_user(tag)<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# Set the variables for the box we will be reading.<br />&nbsp;&nbsp;&nbsp;&nbsp;min_columns&nbsp;&nbsp;&nbsp;&nbsp;= 5<br />&nbsp;&nbsp;&nbsp;&nbsp;user_column&nbsp;&nbsp;&nbsp;&nbsp;= 1<br />&nbsp;&nbsp;&nbsp;&nbsp;subject_column = 3<br />&nbsp;&nbsp;&nbsp;&nbsp;date_column&nbsp;&nbsp;&nbsp;&nbsp;= 4<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;if in_inbox:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;min_columns&nbsp;&nbsp;&nbsp;&nbsp;= 6<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;user_column&nbsp;&nbsp;&nbsp;&nbsp;= 2<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;subject_column = 4<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;date_column&nbsp;&nbsp;&nbsp;&nbsp;= 5<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# Read the rows from this page of the index.<br />&nbsp;&nbsp;&nbsp;&nbsp;for row in tag.find_all(id=re.compile(&quot;^m_\\\\d+$&quot;)):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;columns = [*row.find_all(&quot;td&quot;, recursive=False)]<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if len(columns) &lt; min_columns:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Malformed row?&nbsp;&nbsp;Skip it.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;other_user = columns[user_column].get_text().strip()<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;subject&nbsp;&nbsp;&nbsp;&nbsp;= columns[subject_column].get_text().strip()<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;date&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = parse_date(columns[date_column].get_text().strip())<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg_id&nbsp;&nbsp;&nbsp;&nbsp; = int(row[&quot;id&quot;][2:])<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if in_inbox:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;yield MessageMetadata(msg_id, other_user, user, subject, date, None)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;yield MessageMetadata(msg_id, user, other_user, subject, date, None)<br /><br />def read_box(is_inbox):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;A generator which extracts rows from a box.<br />&nbsp;&nbsp;&nbsp;&nbsp;If is_inbox is True operates over the inbox, otherwise it operates<br />&nbsp;&nbsp;&nbsp;&nbsp;over the sent items.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;global latest_inbox, latest_sent, early_quit<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# Set the variables for the box we will be reading.<br />&nbsp;&nbsp;&nbsp;&nbsp;latest = latest_inbox if is_inbox else latest_sent<br />&nbsp;&nbsp;&nbsp;&nbsp;end_of_new_messages = False<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;next_page = &quot;<a href=\"https://inkbunny.net/privatemessages_process.php?mode=&quot\" rel=\"nofollow\">https://inkbunny.net/privatemessages_process.php?mode=&...</a>;<br />&nbsp;&nbsp;&nbsp;&nbsp;if is_inbox:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;Reading Inbox.&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next_page += &quot;inbox&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;else:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;Reading Sent.&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next_page += &quot;sent&quot;<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# Download and parse the next page of the index.<br />&nbsp;&nbsp;&nbsp;&nbsp;while next_page is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next_page = urllib.parse.urljoin(base_url, next_page)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;soup = download(next_page)<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Read the rows in the index and yield them to the caller.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for row in read_box_page(soup, is_inbox):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if row.msg_id &lt;= latest:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;end_of_new_messages = True<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;yield row<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Exit the loop early if we&#039;ve encountered a message on this page<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# older than one we&#039;ve already downloaded.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if early_quit and end_of_new_messages:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;End of new messages in this box.&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;break<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next_page = get_next_page(soup)<br /><br />def unparse_html_to_bbcode(tag):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Given some HTML generated by Inkbunny&#039;s BBCode renderer attempt to return<br />&nbsp;&nbsp;&nbsp;&nbsp;the BBCode which might have generated it.&nbsp;&nbsp;Since the transformation is<br />&nbsp;&nbsp;&nbsp;&nbsp;non-injective it&#039;s impossible to reverse with 100% accuracy.<br />&nbsp;&nbsp;&nbsp;&nbsp;White-space will likely not be preserved either.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;global simple_replacements, offsite_mapping, re_offsite_title, re_usericon, re_color, re_size, re_pool_url, re_bare<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# [code]<br />&nbsp;&nbsp;&nbsp;&nbsp;for string in tag.find_all(string=re_bare):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;string.replace_with(re_bare.sub(lambda m: &quot;[code]&quot;+m.group(0)+&quot;[​/code]&quot;, string.string))<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# [q] and [q=someone]<br />&nbsp;&nbsp;&nbsp;&nbsp;for quote in tag.find_all(class_=&quot;bbcode_quote&quot;):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;author = quote.find(class_=&quot;bbcode_quote_author&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;body = quote.find(class_=&quot;bbcode_quote_quote&quot;).extract()<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;argument = &quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if author is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;argument = &quot;=&quot; + author.get_text().strip()[:-7]<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;quote.insert_before(bs4.NavigableString(&quot;[q&quot;+argument+&quot;]&quot;))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;quote.insert_before(body)<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;children = (*body.contents,)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if len(children) == 1:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if isinstance(children[0], bs4.NavigableString):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;children[0].replace_with(children[0].string.strip())<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;elif len(children) &gt; 1:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if isinstance(children[0], bs4.NavigableString):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;children[0].replace_with(children[0].string.lstrip())<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if isinstance(children[-1], bs4.NavigableString):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;children[-1].replace_with(children[-1].string.rstrip())<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;body.unwrap()<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;quote.insert_after(bs4.NavigableString(&quot;[/q]&quot;))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;quote.extract()<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# [smallpool], [mediumpool]<br />&nbsp;&nbsp;&nbsp;&nbsp;for pool in tag.find_all(class_=&quot;widget_imageFromSubmission&quot;):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;table = pool.find_parent(&quot;table&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if table is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;table = table.find_parent(&quot;table&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if table is None or table.parent is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# There are three divs with widget_imageFromSubmissions as their<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# class per pool table, so it is possible we&#039;ve already processed<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# this pool.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# This also filters out thumbnails which get processed after.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;size = re_size.search(pool.find(&quot;img&quot;)[&quot;src&quot;]).group(1)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pool_id = table.find(&quot;a&quot;, href=re_pool_url)[&quot;href&quot;][31:]<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;table.replace_with(bs4.NavigableString(&quot;[&quot;+size+&quot;pool]&quot;+pool_id+&quot;[/&quot;+size+&quot;pool]&quot;))<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# [smallthumb], [mediumthumb], [largethumb], [hugethumb]<br />&nbsp;&nbsp;&nbsp;&nbsp;for thumb in tag.find_all(class_=&quot;widget_imageFromSubmission&quot;):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;table = thumb.find_parent(&quot;table&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;size = re_size.search(thumb.find(&quot;img&quot;)[&quot;src&quot;]).group(1)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;submission_id = thumb.find(&quot;a&quot;)[&quot;href&quot;][3:]<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;table.replace_with(bs4.NavigableString(&quot;[&quot;+size+&quot;thumb]&quot;+submission_id+&quot;[/&quot;+size+&quot;thumb]&quot;))<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# [color]<br />&nbsp;&nbsp;&nbsp;&nbsp;for color in tag.find_all(&quot;span&quot;, style=re_color):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;color.insert_before(bs4.NavigableString(&quot;[color=&quot;+color[&quot;style&quot;][7:-1]+&quot;]&quot;))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;color.insert_after(bs4.NavigableString(&quot;[/color]&quot;))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;color.unwrap()<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# Newlines<br />&nbsp;&nbsp;&nbsp;&nbsp;for br in tag.find_all(&quot;br&quot;):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;br.replace_with(bs4.NavigableString(&quot;\\n&quot;))<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# [icon], [iconname]<br />&nbsp;&nbsp;&nbsp;&nbsp;for icon in tag.find_all(&quot;img&quot;, src=re_usericon):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;table = icon.find_parent(&quot;table&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;link = icon.find_parent(&quot;a&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;name = link[&quot;href&quot;][21:]<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if len((*table.find_all(&quot;a&quot;),)) &gt; 1:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;table.replace_with(&quot;[iconname]&quot;+name+&quot;[/iconname]&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;table.replace_with(&quot;[icon]&quot;+name+&quot;[/icon]&quot;)<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# [name]<br />&nbsp;&nbsp;&nbsp;&nbsp;for namelink in tag.find_all(&quot;span&quot;, class_=&quot;widget_userNameSmall&quot;):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;name = namelink.find(&quot;a&quot;)[&quot;href&quot;][1:]<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;namelink.replace_with(&quot;[name]&quot;+name+&quot;[/name]&quot;)<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# [da], [fa], [sf], [w]<br />&nbsp;&nbsp;&nbsp;&nbsp;for link in tag.find_all(&quot;a&quot;, title=re_offsite_title):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Each off-site link generates two sibling elements in the output, we<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# can replace either, but we should remove the one we don&#039;t replace.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if link.find(&quot;img&quot;) is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Replace the textual link.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m = re_offsite_title.search(link[&quot;title&quot;])<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;site = offsite_mapping[m.group(1)]<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;link.replace_with(&quot;[&quot;+site+&quot;]&quot;+link.get_text().strip()+&quot;[/&quot;+site+&quot;]&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Remove the image link.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;link.extract()<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# [url]<br />&nbsp;&nbsp;&nbsp;&nbsp;for a in tag.find_all(&quot;a&quot;):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a.insert_before(bs4.NavigableString(&quot;[url=&quot;+a[&quot;href&quot;]+&quot;]&quot;))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a.insert_after(bs4.NavigableString(&quot;[/url]&quot;))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a.unwrap()<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# simple replacements<br />&nbsp;&nbsp;&nbsp;&nbsp;for (name, args) in simple_replacements.items():<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for target in tag.find_all(*args[0], **args[1]):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;target.insert_before(bs4.NavigableString(&quot;[&quot;+name+&quot;]&quot;))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;target.insert_after(bs4.NavigableString(&quot;[/&quot;+name+&quot;]&quot;))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;target.unwrap()<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# combine all text nodes and return the string representation of the child<br />&nbsp;&nbsp;&nbsp;&nbsp;# nodes.<br />&nbsp;&nbsp;&nbsp;&nbsp;tag.smooth()<br />&nbsp;&nbsp;&nbsp;&nbsp;return &quot;&quot;.join(str(n) for n in tag.contents)<br /><br />def read_thread(tag, other_user):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;A generator which extracts messages from thread view.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;global user, messages<br />&nbsp;&nbsp;&nbsp;&nbsp;previous_subject = None<br />&nbsp;&nbsp;&nbsp;&nbsp;previous_msg_id&nbsp;&nbsp;= None<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;got_result = False<br />&nbsp;&nbsp;&nbsp;&nbsp;for elem in tag.find_all(id=re.compile(&quot;^irt_message_\\\\d+$&quot;)):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Extract the next message&#039;s data from the page.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg_id = int(elem[&quot;id&quot;][12:])<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;children = [e for e in elem.children if not isinstance(e, bs4.NavigableString)]<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if len(children) &lt; 5:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Malformed<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;date_field&nbsp;&nbsp;&nbsp;&nbsp;= children[2].find(True)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;body_span&nbsp;&nbsp;&nbsp;&nbsp; = children[2].find(&quot;span&quot;, style=&quot;word-wrap: break-word;&quot;)<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if date_field is None or body_span is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Malformed<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;subject_div = children[2].find(style=&quot;margin-bottom: 5px;&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;left_link&nbsp;&nbsp; = children[0].find(class_=&quot;widget_userNameSmall&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;right_link&nbsp;&nbsp;= children[4].find(class_=&quot;widget_userNameSmall&quot;)<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;date&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= parse_date(date_field.get_text().strip())<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;subject&nbsp;&nbsp;&nbsp;&nbsp; = subject_div.get_text().strip() if subject_div is not None else previous_subject<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;in_reply_to = previous_msg_id<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sender&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= None<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if left_link is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sender = left_link.get_text().strip()<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;elif right_link is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sender = right_link.get_text().strip()<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;receiver = other_user if sender == user else user<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Create, populate and yield an EmailMessage for this message.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;content = bs4.BeautifulSoup(&quot;&lt;html&gt;&lt;title/&gt;&lt;body/&gt;&quot;, features=&quot;lxml&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;content.title.string = subject<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;content.body.append(body_span)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;body_span.unwrap()<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;metadata = MessageMetadata(msg_id, sender, receiver, subject, date, in_reply_to)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg = metadata.create_message()<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg.add_alternative(&quot;&lt;!DOCTYPE html&gt;&quot; + str(content), &quot;html&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg.add_alternative(str(unparse_html_to_bbcode(content.body)))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;yield msg<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;got_result = True<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Update information on the previous message (the one we just yielded)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# prior to extracting next message.<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if subject is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;previous_subject = subject<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;previous_msg_id = msg_id<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;# This should only be reached if a page other than the one we were<br />&nbsp;&nbsp;&nbsp;&nbsp;# expecting is returned, i.e. an error page or similar.<br />&nbsp;&nbsp;&nbsp;&nbsp;# Since something has clearly gone wrong just inform the user and abort.<br />&nbsp;&nbsp;&nbsp;&nbsp;if not got_result:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.critical(&quot;Failed to parse at least one message in thread.&nbsp;&nbsp;Got: %s&quot;, str(tag))<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit(1)<br /><br /># Main Program starts here.<br /><br /># Configure logging.<br />log.addHandler(logging.StreamHandler())<br />log.setLevel(logging.INFO)<br /><br /># Handle arguments.<br />arg_parser = argparse.ArgumentParser(<br />&nbsp;&nbsp;&nbsp;&nbsp;prog = program_file,<br />&nbsp;&nbsp;&nbsp;&nbsp;description = &quot;&quot;.join((<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Downloads messages from the Inkbunny.net website and stores them&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot; as emails in maildir format.&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;)),<br />&nbsp;&nbsp;&nbsp;&nbsp;epilog = &quot;&quot;.join((<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;If no DIRECTORY is specified then the current directory is&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot; assumed to be the maildir, care should be taken to ensure that&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot; the program is run in the correct directory or it may read or&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot; write to files the user does not want it to.\\n&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;\\n&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;The SESSION may be passed using the PHPSESSID environment variable.&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot; this may be useful if you wish to pass the same value to multiple&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot; programs, or if you wish to set the value in your shell&#039;s profile.\\n&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;\\n&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Report bugs to: &quot;+bug_reports+&quot;\\n&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;program_name + &quot; home page: &lt;&quot;+homepage+&quot;\\n&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;)),<br />&nbsp;&nbsp;&nbsp;&nbsp;formatter_class = argparse.RawDescriptionHelpFormatter<br />)<br />arg_parser.add_argument(<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;-s&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;--session&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;nargs = 1,<br />&nbsp;&nbsp;&nbsp;&nbsp;help = &quot;the session id to send with requests&quot;<br />)<br />arg_parser.add_argument(<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;-C&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;--directory&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;nargs = 1,<br />&nbsp;&nbsp;&nbsp;&nbsp;help = &quot;change directory when program starts&quot;<br />)<br />arg_parser.add_argument(<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;-q&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;--quiet&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;action = &quot;count&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;default = 2,<br />&nbsp;&nbsp;&nbsp;&nbsp;help = &quot;decrease verbosity&quot;<br />)<br />arg_parser.add_argument(<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;-v&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;--verbose&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;action = &quot;count&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;default = 0,<br />&nbsp;&nbsp;&nbsp;&nbsp;help = &quot;increase verbosity&quot;<br />)<br />arg_parser.add_argument(<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;-f&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;--full-scan&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;action = &quot;store_true&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;help = &quot;read the full message index even if it seems unnecessary&quot;<br />)<br />arg_parser.add_argument(<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;-V&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;--version&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;action = &quot;store_true&quot;<br />)<br /><br />args = arg_parser.parse_args()<br /><br />if args.version:<br />&nbsp;&nbsp;&nbsp;&nbsp;print(<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot; &quot;.join((program_name, version)),<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Copyright (C) 2025 JustLurking&lt;<a href=\"https://inkbunny.net/JustLurking&gt;&quot;\" rel=\"nofollow\">https://inkbunny.net/JustLurking&gt;&quot;</a>,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;License GPLv3+: GNU GPL version 3 or later &quot;+<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&lt;<a href=\"https://gnu.org/licenses/gpl.html&gt;&quot;\" rel=\"nofollow\">https://gnu.org/licenses/gpl.html&gt;&quot;</a>,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;This is free software: you are free to change and redistribute it.&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;There is NO WARRANTY, to the extent permitted by law.&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sep=&quot;\\n&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;)<br />&nbsp;&nbsp;&nbsp;&nbsp;exit(0)<br /><br />log.setLevel(max(1, min(5, args.quiet-args.verbose))*10)<br /><br />if args.directory is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;Changing to directory %d.&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;os.chdir(args.directory)<br /><br />if args.full_scan:<br />&nbsp;&nbsp;&nbsp;&nbsp;early_quit = False<br /><br />if args.session is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;cookie = args.session<br /><br /># Open or create Maildir.<br />maildir = mailbox.Maildir(&quot;.&quot;)<br />inbox&nbsp;&nbsp; = maildir.add_folder(&quot;inbox&quot;)<br />sent&nbsp;&nbsp;&nbsp;&nbsp;= maildir.add_folder(&quot;sent&quot;)<br /><br /># Set the Session Cookie from the environment.<br />if cookie is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;cookie = os.getenv(&quot;PHPSESSID&quot;)<br /><br />if cookie is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;log.error(&quot;Please use the -s argument or set the PHPSESSID environment variable to set the session id.&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;exit(1)<br />cookies.set(&quot;PHPSESSID&quot;, cookie, domain=&quot;inkbunny.net&quot;, path=&quot;/&quot;)<br /><br /># Make a list of all already downloaded messages.<br />for msg in inbox:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg_id = int(msg[&quot;Message-Id&quot;])<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;downloaded.add(msg_id)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;latest_inbox = max(latest_inbox, msg_id)<br /><br />for msg in sent:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg_id = int(msg[&quot;Message-Id&quot;])<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;downloaded.add(msg_id)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;latest_sent = max(latest_sent, msg_id)<br /><br /># Fetch message indices for inbox and sent and for each message found fetch the<br /># appropriate thread view and download all new messages in that thread.<br />for row in read_box(True):<br />&nbsp;&nbsp;&nbsp;&nbsp;row.download_messages_in_thread()<br /><br />for row in read_box(False):<br />&nbsp;&nbsp;&nbsp;&nbsp;row.download_messages_in_thread()<br /></span>",
  "pools_count": 2,
  "title": "Inkbunny Maildir Fetch 0.1.0",
  "deleted": "f",
  "public": "t",
  "mimetype": "text/plain",
  "pagecount": "1",
  "rating_id": "0",
  "rating_name": "General",
  "ratings": [],
  "submission_type_id": "12",
  "type_name": "Writing - Document",
  "guest_block": "f",
  "friends_only": "f",
  "comments_count": "2",
  "views": "68"
}