Onionr/onionr/static-data/default-plugins/pms/main.py

324 lines
12 KiB
Python
Executable File

'''
Onionr - Private P2P Communication
This default plugin handles private messages in an email like fashion
'''
'''
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
# Imports some useful libraries
import logger, config, threading, time, datetime
from onionrblockapi import Block
import onionrexceptions
from onionrusers import onionrusers
from onionrutils import stringvalidators, escapeansi, bytesconverter
import locale, sys, os, json
locale.setlocale(locale.LC_ALL, '')
plugin_name = 'pms'
PLUGIN_VERSION = '0.0.1'
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
import sentboxdb, mailapi, loadinbox # import after path insert
flask_blueprint = mailapi.flask_blueprint
def draw_border(text):
# This function taken from https://stackoverflow.com/a/20757491 by https://stackoverflow.com/users/816449/bunyk, under https://creativecommons.org/licenses/by-sa/3.0/
lines = text.splitlines()
width = max(len(s) for s in lines)
res = ['' + '' * width + '']
for s in lines:
res.append('' + (s + ' ' * width)[:width] + '')
res.append('' + '' * width + '')
return '\n'.join(res)
class MailStrings:
def __init__(self, mailInstance):
self.mailInstance = mailInstance
self.programTag = 'OnionrMail v%s' % (PLUGIN_VERSION)
choices = ['view inbox', 'view sentbox', 'send message', 'toggle pseudonymity', 'quit']
self.mainMenuChoices = choices
self.mainMenu = '''-----------------
1. %s
2. %s
3. %s
4. %s
5. %s''' % (choices[0], choices[1], choices[2], choices[3], choices[4])
class OnionrMail:
def __init__(self, pluginapi):
self.myCore = pluginapi.get_core()
self.strings = MailStrings(self)
self.sentboxTools = sentboxdb.SentBox(self.myCore)
self.sentboxList = []
self.sentMessages = {}
self.doSigs = True
return
def inbox(self):
blockCount = 0
pmBlockMap = {}
pmBlocks = {}
logger.info('Decrypting messages...', terminal=True)
choice = ''
displayList = []
subject = ''
# this could use a lot of memory if someone has received a lot of messages
for blockHash in self.myCore.getBlocksByType('pm'):
pmBlocks[blockHash] = Block(blockHash, core=self.myCore)
pmBlocks[blockHash].decrypt()
blockCount = 0
for blockHash in pmBlocks:
if not pmBlocks[blockHash].decrypted:
continue
blockCount += 1
pmBlockMap[blockCount] = blockHash
block = pmBlocks[blockHash]
senderKey = block.signer
try:
senderKey = senderKey.decode()
except AttributeError:
pass
senderDisplay = onionrusers.OnionrUser(self.myCore, senderKey).getName()
if senderDisplay == 'anonymous':
senderDisplay = senderKey
blockDate = pmBlocks[blockHash].getDate().strftime("%m/%d %H:%M")
try:
subject = pmBlocks[blockHash].bmetadata['subject']
except KeyError:
subject = ''
displayList.append('%s. %s - %s - <%s>: %s' % (blockCount, blockDate, senderDisplay[:12], subject[:10], blockHash))
while choice not in ('-q', 'q', 'quit'):
for i in displayList:
logger.info(i, terminal=True)
try:
choice = logger.readline('Enter a block number, -r to refresh, or -q to stop: ').strip().lower()
except (EOFError, KeyboardInterrupt):
choice = '-q'
if choice in ('-q', 'q', 'quit'):
continue
if choice in ('-r', 'r', 'refresh'):
# dirty hack
self.inbox()
return
try:
choice = int(choice)
except ValueError:
pass
else:
try:
pmBlockMap[choice]
readBlock = pmBlocks[pmBlockMap[choice]]
except KeyError:
pass
else:
cancel = ''
readBlock.verifySig()
senderDisplay = bytesconverter.bytes_to_str(readBlock.signer)
if len(senderDisplay.strip()) == 0:
senderDisplay = 'Anonymous'
logger.info('Message received from %s' % (senderDisplay,), terminal=True)
logger.info('Valid signature: %s' % readBlock.validSig, terminal=True)
if not readBlock.validSig:
logger.warn('This message has an INVALID/NO signature. ANYONE could have sent this message.', terminal=True)
cancel = logger.readline('Press enter to continue to message, or -q to not open the message (recommended).')
print('')
if cancel != '-q':
try:
print(draw_border(escapeansi.escape_ANSI(readBlock.bcontent.decode().strip())))
except ValueError:
logger.warn('Error presenting message. This is usually due to a malformed or blank message.', terminal=True)
pass
if readBlock.validSig:
reply = logger.readline("Press enter to continue, or enter %s to reply" % ("-r",))
print('')
if reply == "-r":
self.draft_message(bytesconverter.bytes_to_str(readBlock.signer,))
else:
logger.readline("Press enter to continue")
print('')
return
def sentbox(self):
'''
Display sent mail messages
'''
entering = True
while entering:
self.get_sent_list()
logger.info('Enter a block number or -q to return', terminal=True)
try:
choice = input('>')
except (EOFError, KeyboardInterrupt) as e:
entering = False
else:
try:
choice = int(choice) - 1
except ValueError:
pass
else:
try:
self.sentboxList[int(choice)]
except (IndexError, ValueError) as e:
logger.warn('Invalid block.', terminal=True)
else:
logger.info('Sent to: ' + self.sentMessages[self.sentboxList[int(choice)]][1], terminal=True)
# Print ansi escaped sent message
logger.info(escapeansi.escape_ANSI(self.sentMessages[self.sentboxList[int(choice)]][0]), terminal=True)
input('Press enter to continue...')
finally:
if choice == '-q':
entering = False
return
def get_sent_list(self, display=True):
count = 1
self.sentboxList = []
self.sentMessages = {}
for i in self.sentboxTools.listSent():
self.sentboxList.append(i['hash'])
self.sentMessages[i['hash']] = (bytesconverter.bytes_to_str(i['message']), i['peer'], i['subject'])
if display:
logger.info('%s. %s - %s - (%s) - %s' % (count, i['hash'], i['peer'][:12], i['subject'], i['date']), terminal=True)
count += 1
return json.dumps(self.sentMessages)
def draft_message(self, recip=''):
message = ''
newLine = ''
subject = ''
entering = False
if len(recip) == 0:
entering = True
while entering:
try:
recip = logger.readline('Enter peer address, or -q to stop:').strip()
if recip in ('-q', 'q'):
raise EOFError
if not stringvalidators.validate_pub_key(recip):
raise onionrexceptions.InvalidPubkey('Must be a valid ed25519 base32 encoded public key')
except onionrexceptions.InvalidPubkey:
logger.warn('Invalid public key', terminal=True)
except (KeyboardInterrupt, EOFError):
entering = False
else:
break
else:
# if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key
return
try:
subject = logger.readline('Message subject: ')
except (KeyboardInterrupt, EOFError):
pass
cancelEnter = False
logger.info('Enter your message, stop by entering -q on a new line. -c to cancel', terminal=True)
while newLine != '-q':
try:
newLine = input()
except (KeyboardInterrupt, EOFError):
cancelEnter = True
if newLine == '-c':
cancelEnter = True
break
if newLine == '-q':
continue
newLine += '\n'
message += newLine
if not cancelEnter:
logger.info('Inserting encrypted message as Onionr block....', terminal=True)
blockID = self.myCore.insertBlock(message, header='pm', encryptType='asym', asymPeer=recip, sign=self.doSigs, meta={'subject': subject})
def toggle_signing(self):
self.doSigs = not self.doSigs
def menu(self):
choice = ''
while True:
sigMsg = 'Message Signing: %s'
logger.info(self.strings.programTag + '\n\nUser ID: ' + self.myCore._crypto.pubKey, terminal=True)
if self.doSigs:
sigMsg = sigMsg % ('enabled',)
else:
sigMsg = sigMsg % ('disabled (Your messages cannot be trusted)',)
if self.doSigs:
logger.info(sigMsg, terminal=True)
else:
logger.warn(sigMsg, terminal=True)
logger.info(self.strings.mainMenu.title(), terminal=True) # print out main menu
try:
choice = logger.readline('Enter 1-%s:\n' % (len(self.strings.mainMenuChoices))).lower().strip()
except (KeyboardInterrupt, EOFError):
choice = '5'
if choice in (self.strings.mainMenuChoices[0], '1'):
self.inbox()
elif choice in (self.strings.mainMenuChoices[1], '2'):
self.sentbox()
elif choice in (self.strings.mainMenuChoices[2], '3'):
self.draft_message()
elif choice in (self.strings.mainMenuChoices[3], '4'):
self.toggle_signing()
elif choice in (self.strings.mainMenuChoices[4], '5'):
logger.info('Goodbye.', terminal=True)
break
elif choice == '':
pass
else:
logger.warn('Invalid choice.', terminal=True)
return
def add_deleted(keyStore, bHash):
existing = keyStore.get('deleted_mail')
if existing is None:
existing = []
else:
if bHash in existing:
return
keyStore.put('deleted_mail', existing.append(bHash))
def on_insertblock(api, data={}):
meta = json.loads(data['meta'])
if meta['type'] == 'pm':
sentboxTools = sentboxdb.SentBox(api.get_core())
sentboxTools.addToSent(data['hash'], data['peer'], data['content'], meta['subject'])
def on_init(api, data = None):
'''
This event is called after Onionr is initialized, but before the command
inputted is executed. Could be called when daemon is starting or when
just the client is running.
'''
pluginapi = api
mail = OnionrMail(pluginapi)
api.commands.register(['mail'], mail.menu)
api.commands.register_help('mail', 'Interact with OnionrMail')
return