{
  "submission_id": "3528036",
  "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-18 16:03:57.7094+00",
  "create_datetime_usertime": "18 Jan 2025 17:03 CET",
  "last_file_update_datetime": "2025-01-18 16:02:28.52135+00",
  "last_file_update_datetime_usertime": "18 Jan 2025 17:02 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": "5395588_JustLurking_ib-sendmail.txt",
  "file_url_full": "https://nl1.ib.metapix.net/files/full/5395/5395588_JustLurking_ib-sendmail.txt",
  "file_url_screen": "https://nl1.ib.metapix.net/files/screen/5395/5395588_JustLurking_ib-sendmail.txt",
  "file_url_preview": "https://nl1.ib.metapix.net/files/preview/5395/5395588_JustLurking_ib-sendmail.txt",
  "thumbnail_url_huge": "https://nl1.ib.metapix.net/thumbnails/huge/5395/5395588_JustLurking_ib-sendmail.jpg",
  "thumbnail_url_large": "https://nl1.ib.metapix.net/thumbnails/large/5395/5395588_JustLurking_ib-sendmail.jpg",
  "thumbnail_url_medium": "https://nl1.ib.metapix.net/thumbnails/medium/5395/5395588_JustLurking_ib-sendmail.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": "5395588",
      "file_name": "5395588_JustLurking_ib-sendmail.txt",
      "file_url_full": "https://nl1.ib.metapix.net/files/full/5395/5395588_JustLurking_ib-sendmail.txt",
      "file_url_screen": "https://nl1.ib.metapix.net/files/screen/5395/5395588_JustLurking_ib-sendmail.txt",
      "file_url_preview": "https://nl1.ib.metapix.net/files/preview/5395/5395588_JustLurking_ib-sendmail.txt",
      "mimetype": "text/plain",
      "submission_id": "3528036",
      "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": "aa9cfdc12316c4822369022d02525f7f",
      "full_file_md5": "aa9cfdc12316c4822369022d02525f7f",
      "large_file_md5": "",
      "small_file_md5": "",
      "thumbnail_md5": "375784883e2cc4942ba214f9ea11fb7f",
      "deleted": "f",
      "create_datetime": "2025-01-18 16:02:28.52135+00",
      "create_datetime_usertime": "18 Jan 2025 17:02 CET",
      "thumbnail_url_huge": "https://nl1.ib.metapix.net/thumbnails/huge/5395/5395588_JustLurking_ib-sendmail.jpg",
      "thumbnail_url_large": "https://nl1.ib.metapix.net/thumbnails/large/5395/5395588_JustLurking_ib-sendmail.jpg",
      "thumbnail_url_medium": "https://nl1.ib.metapix.net/thumbnails/medium/5395/5395588_JustLurking_ib-sendmail.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": "98759",
      "name": "Inkbunny Sendmail",
      "description": "Software for posting private messages using compatible email clients.",
      "count": "1"
    }
  ],
  "description": "A program which posts plain-text emails to Inkbunny as private messages.  Supports threading via an optional 'In-Reply-To' header.\n\nBe careful what automation you setup with this.  No one likes a spammer least of all the site admins you will piss off if you misuse this script.\n\nThe script is intended to be called from a mail client that can be configured to use a sendmail-compatible mail submission agent (MSA) but it can be run from the command line as so:\n\n[q]export PHPSESSID='my-session-id'\nib-sendmail -t <<EOF\nTo: JustLurking\nFrom: MyUserName\nIn-Reply-To: <55512345>\nSubject: This message was posted with ib-sendmail\n\nI'm a [i]ib-sendmail[/i] user.\n\nEOF[/q]\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 sending 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\nNot all email messages can be sent via ib-sendmail.  Rather than trying to convert feature rich emails and guess at the user's intent the program simply refuses to post an email where there is any ambiguity.  The following conditions must be met for an email to be posted by ib-sendmail:\n\n1. Before any connection is made to the internet the message must have:\na. one text/plain section\nb. zero or more multipart/* sections\nc. one recipient (either as an argument or in a To: header if -t is given)\nd. zero Cc: or Bcc: headers\ne. one sender (either in a From: header or given as an argument to -f)\nf. Both must have only a local part with no @ in the addresses.\ng. If there is an In-Reply-To it must be numeric and singular.\nh. There must be a subject header containing non-whitespace characters\n\n2. Once a connection has been made to Inkbunny:\na. The sender must be the logged in user.\nb. The recipient must be the other user in the thread if an In-Reply-To header is set.\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=https://wiki.inkbunny.net/wiki/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 posts plain-text emails to Inkbunny as private messages.&nbsp;&nbsp;Supports threading via an optional &#039;In-Reply-To&#039; header.<br /><br />Be careful what automation you setup with this.&nbsp;&nbsp;No one likes a spammer least of all the site admins you will piss off if you misuse this script.<br /><br />The script is intended to be called from a mail client that can be configured to use a sendmail-compatible mail submission agent (MSA) but it can be run from the command line as so:<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 />ib-sendmail -t &lt;&lt;EOF<br />To: JustLurking<br />From: MyUserName<br />In-Reply-To: &lt;55512345&gt;<br />Subject: This message was posted with ib-sendmail<br /><br />I&#039;m a <em>ib-sendmail</em> user.<br /><br />EOF\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 />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 sending 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 />Not all email messages can be sent via ib-sendmail.&nbsp;&nbsp;Rather than trying to convert feature rich emails and guess at the user&#039;s intent the program simply refuses to post an email where there is any ambiguity.&nbsp;&nbsp;The following conditions must be met for an email to be posted by ib-sendmail:<br /><br />1. Before any connection is made to the internet the message must have:<br />a. one text/plain section<br />b. zero or more multipart/* sections<br />c. one recipient (either as an argument or in a To: header if -t is given)<br />d. zero Cc: or Bcc: headers<br />e. one sender (either in a From: header or given as an argument to -f)<br />f. Both must have only a local part with no @ in the addresses.<br />g. If there is an In-Reply-To it must be numeric and singular.<br />h. There must be a subject header containing non-whitespace characters<br /><br />2. Once a connection has been made to Inkbunny:<br />a. The sender must be the logged in user.<br />b. The recipient must be the other user in the thread if an In-Reply-To header is set.<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=\"https://wiki.inkbunny.net/wiki/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 Sendmail 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 read an email containing single plain text message then\n# connect to Inkbunny using the session cookie in the PHPSESSID environment\n# variable and attempt to post the message to the user in the From field\n# and in the thread identified by the In-Reply-To field.\n#\n# Not all email messages can be sent via ib-sendmail.  Rather than trying\n# to convert feature rich emails and guess at the user's intent the program\n# simply refuses to post an email where there is any ambiguity.  The following\n# conditions must be met for an email to be posted by ib-sendmail:\n#\n# 1. Before any connection is made to the internet the message must have:\n# a. one text/plain section\n# b. zero or more multipart/* sections\n# c. one recipient (either as an argument or in a To: header if -t is given)\n# d. zero Cc: or Bcc: headers\n# e. one sender (either in a From: header or given as an argument to -f)\n# f. Both must have only a local part with no @ in the addresses.\n# g. If there is an In-Reply-To it must be numeric and singular.\n# h. There must be a subject header containing non-whitespace characters\n#\n# 2. Once a connection has been made to Inkbunny:\n# a. The sender must be the logged in user.\n# b. The recipient must be the other user in the thread if an In-Reply-To\n#    header is set.\n\n# Changelog\n# 2025-01-18 JustLurking: Initial Release.\n\nimport argparse\nimport bs4\nimport email\nimport email.policy\nimport logging\nimport os\nimport re\nimport requests\nimport sys\n\n# Variables used throughout the program.\n\n# Used for identifying the program.\nprogram_file  = \"ib-sendmail\"\nprogram_name  = \"Inkbunny Sendmail\"\nversion       = \"0.1.0\"\nbug_reports   = \"https://Inkbunny.net/JustLurking/\"\nhomepage      = \"https://inkbunny.net/submissionsviewall.php?mode=pool&pool_id=98759\n\n# Used for logging.\nlog           = logging.getLogger(__name__)\n\n# Used to download pages.\nbase_url      = \"https://inkbunny.net/privatemessages.php\"\ncookie        = None\ncookies       = requests.cookies.RequestsCookieJar()\n\n# Used to validate input.\nre_valid_email = re.compile(\"^[0-9a-zA-Z]+$\")\nre_valid_message_id = re.compile(\"^<?([0-9]+)>?$\")\n\n# Used to detect errors.\nre_error = re.compile('error$')\nre_footer = re.compile(\"\\\\bfooter\\\\b\")\n\ndef download(url, **kwargs):\n    \"\"\"\n    Utility wrapper for boiler plate around downloading and parsing pages.\n    \"\"\"\n    global cookies\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    return bs4.BeautifulSoup(resp.content, features=\"lxml\")\n\ndef get_logged_in_user(tag):\n    \"\"\"\n    Get the logged-in user's name using the supplied HTML.\n    \"\"\"\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    return user\n\ndef get_other_user(tag):\n    \"\"\"\n    Gets the other user's name using the supplied HTML.\n    \"\"\"\n    reply = tag.find(id=\"reply\")\n    if reply is None:\n        log.critical(\"Unable to find other user details on page.\")\n        exit(1)\n    widget = reply.find(class_=\"widget_userNameSmall\")\n    if widget is None:\n        log.critical(\"Unable to find other user details on page.\")\n        exit(1)\n    user = widget.get_text().strip()\n    log.info(\"Recipient is %s.\", user)\n    return user\n\ndef value(page, name):\n    \"\"\"\n    Gets the value of an input field in the supplied HTML.\n    \"\"\"\n    tag = page.find(\"input\", attrs={\"name\": name})\n    return tag[\"value\"] if tag is not None else None\n\ndef report_issues(*issues):\n    \"\"\"\n    Report any issues to the user and exit with a failure status if any are\n    encountered.\n    \"\"\"\n    abort = False\n\n    for (found_issue, *msg) in issues:\n        if found_issue:\n            log.error(*msg)\n            abort = True\n\n    if abort:\n        exit(1)\n\ndef report_site_issue(tag):\n    \"\"\"\n    Report any issues to the user and exit with a failure status if any are\n    encountered.\n    \"\"\"\n    if tag.find('title').text != \"Error | Inkbunny, the Furry Art Community\":\n        return\n\n    msg = None\n    for content in tag.find_all(class_='content'):\n        if content.find_parent(id='usernavigation') is not None:\n            continue\n        if content.find_parent(class_=re_footer) is not None:\n            continue\n        msg = [ line.strip() for line in content.text.splitlines() ]\n\n    if msg is None:\n        print(\"An unknown error occurred.\")\n    else:\n        print('\\n'.join(line for line in msg if line != \"\"))\n\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        \"Post a private message to inkbunny using an email as the source.\"\n    )),\n    epilog = \"\".join((\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    \"-t\",\n    action = \"store_true\",\n    help = \"Read message for recipients. To:, Cc:, and Bcc: lines will be scanned for recipient addresses. The Bcc: line will be deleted before transmission.\"\n)\narg_parser.add_argument(\n    \"-f\",\n    nargs = 1,\n    help = \"name Sets the name of the ''from'' person (i.e., the envelope sender of the mail).\"\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    \"-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    \"--save-final-response\",\n    const = \"ib-sendmail-debug.html\",\n    nargs = '?'\n)\narg_parser.add_argument(\n    \"-V\",\n    \"--version\",\n    action = \"store_true\"\n)\narg_parser.add_argument(\n    \"addresses\",\n    nargs = '*'\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.session is not None:\n    cookie = args.session\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)\n\ncookies.set(\"PHPSESSID\", cookie, domain=\"inkbunny.net\", path=\"/\")\n\n# Parse the email.\nparser = email.parser.BytesParser(policy = email.policy.SMTP)\nmessage = parser.parse(sys.stdin.buffer)\n\n# 1. Check mail.\n# 1.a. one text/plain section and 1.b. zero or more multipart/* sections.\n\nfound_plain_text_part = False\nfound_single_plain_text_part = True\nfound_no_other_part = True\nfor part in message.walk():\n    if part.is_multipart():\n        continue\n    if part.get_content_type() == \"text/plain\":\n        if found_plain_text_part:\n            found_single_plain_text_part = False\n        found_plain_text_part = True\n    else:\n        found_no_other_part = False\n\n# 1.c one To: header if -t given.\n\nto_addresses = args.addresses\nif args.t and \"To\" in message:\n    to_addresses.append(*message.get_all(\"To\", []))\n\nfound_one_to_address = len(to_addresses) == 1\n\n# 1.d. zero Cc: or Bcc: headers\n\nfound_no_cc_or_bcc_addresses = message.get_all(\"Cc\") is None and message.get_all(\"Bcc\") is None\n\n# 1.e. a from header if -f is not given\n\nfrom_addresses = message.get_all(\"From\", [])\nif args.f is not None:\n    from_addresses.append(args.f)\n\nfound_one_from_address = len(from_addresses) == 1\n\n# 1.f. Both must have only a local part.\n\nfound_valid_to_address = found_one_to_address and re_valid_email.match(to_addresses[0]) is not None\nfound_valid_from_address = found_one_from_address and re_valid_email.match(from_addresses[0]) is not None\n\n# 1.g. If there is an In-Reply-To it must be numeric and singular.\n\nin_reply_to = message.get_all(\"In-Reply-To\", ())\nfound_in_reply_to = in_reply_to != ()\nfound_single_in_reply_to = len(in_reply_to) == 1\nin_reply_to_match = re_valid_message_id.match(in_reply_to[0])\nfound_valid_in_reply_to = in_reply_to_match is not None\nif in_reply_to_match is not None:\n    in_reply_to = (in_reply_to_match.group(1),)\n\n# h. There must be a subject header containing non-whitespace characters\n\nfound_valid_subject = message[\"Subject\"].strip() != \"\"\n\n# Report and abort if any errors have been detected.\n\nreport_issues(\n    (not found_plain_text_part,\n        \"Email has no text/plain part.\"),\n    (not found_single_plain_text_part,\n        \"Email should have a single text/plain part.\"),\n    (not found_no_cc_or_bcc_addresses,\n        \"Email should not have CC or BCC recipients.\"),\n    (not found_no_other_part,\n        \"Email should not have non-text/plain parts.\"),\n    (not found_one_from_address,\n        \"Email should have one sender.\"),\n    (not found_one_to_address,\n        \"Email should have one recipient.\"),\n    (found_in_reply_to and not found_single_in_reply_to,\n        \"Email should be in reply to no more than one message.\"),\n    (not found_valid_in_reply_to,\n        \"Email has invalid In-Reply-To header.\"),\n    (not found_valid_from_address,\n        \"Email has invalid sender.\"),\n    (not found_valid_to_address,\n        \"Email has invalid recipient.\"),\n    (not found_valid_subject,\n        \"Email has invalid recipient.\"),\n    (not found_plain_text_part,\n        \"Email has no text/plain part.\")\n)\n\n# 2. Fetch the message submission page.\n\nresp = requests.get(\n    \"https://inkbunny.net/privatemessageview.php\",\n    cookies = cookies,\n    params = {'private_message_id': in_reply_to[0]} if found_in_reply_to else {}\n)\n\nif args.save_final_response is not None:\n    with open(args.save_final_response, \"wb\") as f:\n        f.write(resp.content)\n\nresp.raise_for_status()\n\npage = bs4.BeautifulSoup(resp.content, features=\"lxml\")\nreport_site_issue(page)\nuser = get_logged_in_user(page)\n\n# 2.a. The From must match the logged in user.\n\nfound_matching_sender = user.lower() == from_addresses[0].lower()\n\n# 2.b. The To must match the other user if an In-Reply-To is given.\n\nfound_matching_recipient = True\nif found_in_reply_to:\n    other_user = get_other_user(page)\n    found_matching_recipient = other_user.lower() == to_addresses[0].lower()\n\n# Report and abort if any errors have been detected.\n\nreport_issues(\n    (not found_matching_sender,\n        \"Email has invalid sender.\"),\n    (not found_matching_recipient,\n        \"Email has invalid recipient.\")\n)\n\n# Extract fields from the message submission page.\n\nfields = {};\n\nif found_in_reply_to:\n    # This is a reply\n    fields[\"to_user_id\"]         = value(page, \"to_user_id\")\n    fields[\"private_message_id\"] = value(page, \"private_message_id\")\nelse:\n    # This is a new message\n    fields[\"to_username\"]        = to_addresses[0]\n\nfields[\"token\"]        = value(page, \"token\")\nfields[\"from_user_id\"] = value(page, \"from_user_id\")\n\nfields[\"subject\"]      = message[\"Subject\"]\nfields[\"comment\"]      = message.get_content()\n\nreport_issues(*(\n    (True, \"Unable to extract the value of the %s field from page.\", k)\n    for (k, v) in fields.items() if v is None\n))\n\n# Send message.\n\nresp = requests.post(\n    \"https://inkbunny.net/privatemessageview_process.php\",\n    cookies = cookies,\n    data = fields\n)\n\nif args.save_final_response is not None:\n    with open(args.save_final_response, \"wb\") as f:\n        f.write(resp.content)\n\nresp.raise_for_status()\npage = bs4.BeautifulSoup(resp.content, features=\"lxml\")\nreport_site_issue(page)\n\n# Detect and report non-site errors.\n\nexit_status = 0\nfor error in page.find_all(id=re_error):\n    print(error.text)\n    exit_status = 1\n\nif exit_status == 0:\n    log.info(\"Message sent.\");\n\nexit(exit_status)\n[/code]",
  "writing_bbcode_parsed": "<span style='word-wrap: break-word;'>#!/usr/bin/python3<br /><br /># Inkbunny Sendmail 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 read an email containing single plain text message then<br /># connect to Inkbunny using the session cookie in the PHPSESSID environment<br /># variable and attempt to post the message to the user in the From field<br /># and in the thread identified by the In-Reply-To field.<br />#<br /># Not all email messages can be sent via ib-sendmail.&nbsp;&nbsp;Rather than trying<br /># to convert feature rich emails and guess at the user&#039;s intent the program<br /># simply refuses to post an email where there is any ambiguity.&nbsp;&nbsp;The following<br /># conditions must be met for an email to be posted by ib-sendmail:<br />#<br /># 1. Before any connection is made to the internet the message must have:<br /># a. one text/plain section<br /># b. zero or more multipart/* sections<br /># c. one recipient (either as an argument or in a To: header if -t is given)<br /># d. zero Cc: or Bcc: headers<br /># e. one sender (either in a From: header or given as an argument to -f)<br /># f. Both must have only a local part with no @ in the addresses.<br /># g. If there is an In-Reply-To it must be numeric and singular.<br /># h. There must be a subject header containing non-whitespace characters<br />#<br /># 2. Once a connection has been made to Inkbunny:<br /># a. The sender must be the logged in user.<br /># b. The recipient must be the other user in the thread if an In-Reply-To<br />#&nbsp;&nbsp;&nbsp;&nbsp;header is set.<br /><br /># Changelog<br /># 2025-01-18 JustLurking: Initial Release.<br /><br />import argparse<br />import bs4<br />import email<br />import email.policy<br />import logging<br />import os<br />import re<br />import requests<br />import sys<br /><br /># Variables used throughout the program.<br /><br /># Used for identifying the program.<br />program_file&nbsp;&nbsp;= &quot;ib-sendmail&quot;<br />program_name&nbsp;&nbsp;= &quot;Inkbunny Sendmail&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=98759\" rel=\"nofollow\">https://inkbunny.net/submissionsviewall.php?mode=pool&a...</a><br /><br /># Used for logging.<br />log&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = logging.getLogger(__name__)<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 to validate input.<br />re_valid_email = re.compile(&quot;^[0-9a-zA-Z]+$&quot;)<br />re_valid_message_id = re.compile(&quot;^&lt;?([0-9]+)&gt;?$&quot;)<br /><br /># Used to detect errors.<br />re_error = re.compile(&#039;error$&#039;)<br />re_footer = re.compile(&quot;\\\\bfooter\\\\b&quot;)<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;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;global cookies<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;return bs4.BeautifulSoup(resp.content, features=&quot;lxml&quot;)<br /><br />def get_logged_in_user(tag):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Get the logged-in user&#039;s name using the supplied HTML.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<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 />&nbsp;&nbsp;&nbsp;&nbsp;return user<br /><br />def get_other_user(tag):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Gets the other user&#039;s name using the supplied HTML.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;reply = tag.find(id=&quot;reply&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;if reply is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.critical(&quot;Unable to find other user details on page.&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit(1)<br />&nbsp;&nbsp;&nbsp;&nbsp;widget = reply.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 other 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;Recipient is %s.&quot;, user)<br />&nbsp;&nbsp;&nbsp;&nbsp;return user<br /><br />def value(page, name):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Gets the value of an input field in the supplied HTML.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;tag = page.find(&quot;input&quot;, attrs={&quot;name&quot;: name})<br />&nbsp;&nbsp;&nbsp;&nbsp;return tag[&quot;value&quot;] if tag is not None else None<br /><br />def report_issues(*issues):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Report any issues to the user and exit with a failure status if any are<br />&nbsp;&nbsp;&nbsp;&nbsp;encountered.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;abort = False<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;for (found_issue, *msg) in issues:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if found_issue:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;log.error(*msg)<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;abort = True<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;if abort:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit(1)<br /><br />def report_site_issue(tag):<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;Report any issues to the user and exit with a failure status if any are<br />&nbsp;&nbsp;&nbsp;&nbsp;encountered.<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;<br />&nbsp;&nbsp;&nbsp;&nbsp;if tag.find(&#039;title&#039;).text != &quot;Error | Inkbunny, the Furry Art Community&quot;:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;msg = None<br />&nbsp;&nbsp;&nbsp;&nbsp;for content in tag.find_all(class_=&#039;content&#039;):<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if content.find_parent(id=&#039;usernavigation&#039;) is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if content.find_parent(class_=re_footer) is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg = [ line.strip() for line in content.text.splitlines() ]<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;if msg is None:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(&quot;An unknown error occurred.&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;else:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print(&#039;\\n&#039;.join(line for line in msg if line != &quot;&quot;))<br /><br />&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;Post a private message to inkbunny using an email as the source.&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;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;&gt;\\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;-t&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;action = &quot;store_true&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;help = &quot;Read message for recipients. To:, Cc:, and Bcc: lines will be scanned for recipient addresses. The Bcc: line will be deleted before transmission.&quot;<br />)<br />arg_parser.add_argument(<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;-f&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;nargs = 1,<br />&nbsp;&nbsp;&nbsp;&nbsp;help = &quot;name Sets the name of the &#039;&#039;from&#039;&#039; person (i.e., the envelope sender of the mail).&quot;<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;-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;--save-final-response&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;const = &quot;ib-sendmail-debug.html&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;nargs = &#039;?&#039;<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 />arg_parser.add_argument(<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;addresses&quot;,<br />&nbsp;&nbsp;&nbsp;&nbsp;nargs = &#039;*&#039;<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.session is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;cookie = args.session<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 /><br />cookies.set(&quot;PHPSESSID&quot;, cookie, domain=&quot;inkbunny.net&quot;, path=&quot;/&quot;)<br /><br /># Parse the email.<br />parser = email.parser.BytesParser(policy = email.policy.SMTP)<br />message = parser.parse(sys.stdin.buffer)<br /><br /># 1. Check mail.<br /># 1.a. one text/plain section and 1.b. zero or more multipart/* sections.<br /><br />found_plain_text_part = False<br />found_single_plain_text_part = True<br />found_no_other_part = True<br />for part in message.walk():<br />&nbsp;&nbsp;&nbsp;&nbsp;if part.is_multipart():<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;continue<br />&nbsp;&nbsp;&nbsp;&nbsp;if part.get_content_type() == &quot;text/plain&quot;:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if found_plain_text_part:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;found_single_plain_text_part = False<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;found_plain_text_part = True<br />&nbsp;&nbsp;&nbsp;&nbsp;else:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;found_no_other_part = False<br /><br /># 1.c one To: header if -t given.<br /><br />to_addresses = args.addresses<br />if args.t and &quot;To&quot; in message:<br />&nbsp;&nbsp;&nbsp;&nbsp;to_addresses.append(*message.get_all(&quot;To&quot;, []))<br /><br />found_one_to_address = len(to_addresses) == 1<br /><br /># 1.d. zero Cc: or Bcc: headers<br /><br />found_no_cc_or_bcc_addresses = message.get_all(&quot;Cc&quot;) is None and message.get_all(&quot;Bcc&quot;) is None<br /><br /># 1.e. a from header if -f is not given<br /><br />from_addresses = message.get_all(&quot;From&quot;, [])<br />if args.f is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;from_addresses.append(args.f)<br /><br />found_one_from_address = len(from_addresses) == 1<br /><br /># 1.f. Both must have only a local part.<br /><br />found_valid_to_address = found_one_to_address and re_valid_email.match(to_addresses[0]) is not None<br />found_valid_from_address = found_one_from_address and re_valid_email.match(from_addresses[0]) is not None<br /><br /># 1.g. If there is an In-Reply-To it must be numeric and singular.<br /><br />in_reply_to = message.get_all(&quot;In-Reply-To&quot;, ())<br />found_in_reply_to = in_reply_to != ()<br />found_single_in_reply_to = len(in_reply_to) == 1<br />in_reply_to_match = re_valid_message_id.match(in_reply_to[0])<br />found_valid_in_reply_to = in_reply_to_match is not None<br />if in_reply_to_match is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;in_reply_to = (in_reply_to_match.group(1),)<br /><br /># h. There must be a subject header containing non-whitespace characters<br /><br />found_valid_subject = message[&quot;Subject&quot;].strip() != &quot;&quot;<br /><br /># Report and abort if any errors have been detected.<br /><br />report_issues(<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_plain_text_part,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email has no text/plain part.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_single_plain_text_part,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email should have a single text/plain part.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_no_cc_or_bcc_addresses,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email should not have CC or BCC recipients.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_no_other_part,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email should not have non-text/plain parts.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_one_from_address,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email should have one sender.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_one_to_address,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email should have one recipient.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(found_in_reply_to and not found_single_in_reply_to,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email should be in reply to no more than one message.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_valid_in_reply_to,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email has invalid In-Reply-To header.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_valid_from_address,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email has invalid sender.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_valid_to_address,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email has invalid recipient.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_valid_subject,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email has invalid recipient.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_plain_text_part,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email has no text/plain part.&quot;)<br />)<br /><br /># 2. Fetch the message submission page.<br /><br />resp = requests.get(<br />&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;cookies = cookies,<br />&nbsp;&nbsp;&nbsp;&nbsp;params = {&#039;private_message_id&#039;: in_reply_to[0]} if found_in_reply_to else {}<br />)<br /><br />if args.save_final_response is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;with open(args.save_final_response, &quot;wb&quot;) as f:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;f.write(resp.content)<br /><br />resp.raise_for_status()<br /><br />page = bs4.BeautifulSoup(resp.content, features=&quot;lxml&quot;)<br />report_site_issue(page)<br />user = get_logged_in_user(page)<br /><br /># 2.a. The From must match the logged in user.<br /><br />found_matching_sender = user.lower() == from_addresses[0].lower()<br /><br /># 2.b. The To must match the other user if an In-Reply-To is given.<br /><br />found_matching_recipient = True<br />if found_in_reply_to:<br />&nbsp;&nbsp;&nbsp;&nbsp;other_user = get_other_user(page)<br />&nbsp;&nbsp;&nbsp;&nbsp;found_matching_recipient = other_user.lower() == to_addresses[0].lower()<br /><br /># Report and abort if any errors have been detected.<br /><br />report_issues(<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_matching_sender,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email has invalid sender.&quot;),<br />&nbsp;&nbsp;&nbsp;&nbsp;(not found_matching_recipient,<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;Email has invalid recipient.&quot;)<br />)<br /><br /># Extract fields from the message submission page.<br /><br />fields = {};<br /><br />if found_in_reply_to:<br />&nbsp;&nbsp;&nbsp;&nbsp;# This is a reply<br />&nbsp;&nbsp;&nbsp;&nbsp;fields[&quot;to_user_id&quot;]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = value(page, &quot;to_user_id&quot;)<br />&nbsp;&nbsp;&nbsp;&nbsp;fields[&quot;private_message_id&quot;] = value(page, &quot;private_message_id&quot;)<br />else:<br />&nbsp;&nbsp;&nbsp;&nbsp;# This is a new message<br />&nbsp;&nbsp;&nbsp;&nbsp;fields[&quot;to_username&quot;]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= to_addresses[0]<br /><br />fields[&quot;token&quot;]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= value(page, &quot;token&quot;)<br />fields[&quot;from_user_id&quot;] = value(page, &quot;from_user_id&quot;)<br /><br />fields[&quot;subject&quot;]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= message[&quot;Subject&quot;]<br />fields[&quot;comment&quot;]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= message.get_content()<br /><br />report_issues(*(<br />&nbsp;&nbsp;&nbsp;&nbsp;(True, &quot;Unable to extract the value of the %s field from page.&quot;, k)<br />&nbsp;&nbsp;&nbsp;&nbsp;for (k, v) in fields.items() if v is None<br />))<br /><br /># Send message.<br /><br />resp = requests.post(<br />&nbsp;&nbsp;&nbsp;&nbsp;&quot;<a href=\"https://inkbunny.net/privatemessageview_process.php&quot;\" rel=\"nofollow\">https://inkbunny.net/privatemessageview_process.php&quot;</a>,<br />&nbsp;&nbsp;&nbsp;&nbsp;cookies = cookies,<br />&nbsp;&nbsp;&nbsp;&nbsp;data = fields<br />)<br /><br />if args.save_final_response is not None:<br />&nbsp;&nbsp;&nbsp;&nbsp;with open(args.save_final_response, &quot;wb&quot;) as f:<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;f.write(resp.content)<br /><br />resp.raise_for_status()<br />page = bs4.BeautifulSoup(resp.content, features=&quot;lxml&quot;)<br />report_site_issue(page)<br /><br /># Detect and report non-site errors.<br /><br />exit_status = 0<br />for error in page.find_all(id=re_error):<br />&nbsp;&nbsp;&nbsp;&nbsp;print(error.text)<br />&nbsp;&nbsp;&nbsp;&nbsp;exit_status = 1<br /><br />if exit_status == 0:<br />&nbsp;&nbsp;&nbsp;&nbsp;log.info(&quot;Message sent.&quot;);<br /><br />exit(exit_status)<br /></span>",
  "pools_count": 1,
  "title": "Inkbunny Sendmail 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": "57"
}