2018-07-16 07:40:58 +00:00
'''
2019-06-20 07:59:32 +00:00
Onionr - Private P2P Communication
2018-07-16 07:40:58 +00:00
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
2019-03-06 22:39:46 +00:00
import logger , config , threading , time , datetime
2018-07-16 07:40:58 +00:00
from onionrblockapi import Block
2019-02-17 20:21:03 +00:00
import onionrexceptions
from onionrusers import onionrusers
2019-06-26 00:15:04 +00:00
from onionrutils import stringvalidators , escapeansi , bytesconverter
2019-02-08 18:53:28 +00:00
import locale , sys , os , json
2019-07-17 16:25:29 +00:00
from coredb import blockmetadb
2018-11-03 03:24:14 +00:00
2018-08-17 21:50:16 +00:00
locale . setlocale ( locale . LC_ALL , ' ' )
2018-07-16 07:40:58 +00:00
2019-03-02 06:22:59 +00:00
plugin_name = ' pms '
PLUGIN_VERSION = ' 0.0.1 '
2018-11-03 03:24:14 +00:00
sys . path . insert ( 0 , os . path . dirname ( os . path . realpath ( __file__ ) ) )
2019-03-02 19:17:18 +00:00
import sentboxdb , mailapi , loadinbox # import after path insert
flask_blueprint = mailapi . flask_blueprint
2018-07-16 07:40:58 +00:00
2018-07-19 07:08:51 +00:00
def draw_border ( text ) :
2019-04-27 15:43:16 +00:00
# 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/
2018-07-19 07:08:51 +00:00
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 )
2018-07-16 07:40:58 +00:00
class MailStrings :
def __init__ ( self , mailInstance ) :
self . mailInstance = mailInstance
self . programTag = ' OnionrMail v %s ' % ( PLUGIN_VERSION )
2019-02-05 18:47:11 +00:00
choices = [ ' view inbox ' , ' view sentbox ' , ' send message ' , ' toggle pseudonymity ' , ' quit ' ]
2018-07-16 07:40:58 +00:00
self . mainMenuChoices = choices
2019-02-05 18:47:11 +00:00
self . mainMenu = ''' -----------------
1. % s
2. % s
3. % s
4. % s
5. % s ''' % (choices[0], choices[1], choices[2], choices[3], choices[4])
2018-07-16 07:40:58 +00:00
class OnionrMail :
def __init__ ( self , pluginapi ) :
self . myCore = pluginapi . get_core ( )
self . strings = MailStrings ( self )
2018-11-03 03:24:14 +00:00
self . sentboxTools = sentboxdb . SentBox ( self . myCore )
self . sentboxList = [ ]
self . sentMessages = { }
2019-02-05 18:47:11 +00:00
self . doSigs = True
2018-07-16 07:40:58 +00:00
return
2018-11-11 03:25:40 +00:00
2018-07-16 07:40:58 +00:00
def inbox ( self ) :
2018-07-17 07:18:17 +00:00
blockCount = 0
pmBlockMap = { }
2018-07-18 07:33:23 +00:00
pmBlocks = { }
2019-06-20 00:59:05 +00:00
logger . info ( ' Decrypting messages... ' , terminal = True )
2018-07-18 07:33:23 +00:00
choice = ' '
2018-10-02 16:45:56 +00:00
displayList = [ ]
2019-02-01 19:55:59 +00:00
subject = ' '
2018-07-17 07:18:17 +00:00
2019-03-02 19:17:18 +00:00
# this could use a lot of memory if someone has received a lot of messages
2019-07-17 16:25:29 +00:00
for blockHash in blockmetadb . get_blocks_by_type ( ' pm ' ) :
2018-07-18 07:33:23 +00:00
pmBlocks [ blockHash ] = Block ( blockHash , core = self . myCore )
pmBlocks [ blockHash ] . decrypt ( )
blockCount = 0
2018-12-09 17:29:39 +00:00
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
2018-08-29 01:09:27 +00:00
2018-12-09 17:29:39 +00:00
blockDate = pmBlocks [ blockHash ] . getDate ( ) . strftime ( " % m/ %d % H: % M " )
2019-02-01 19:55:59 +00:00
try :
subject = pmBlocks [ blockHash ] . bmetadata [ ' subject ' ]
except KeyError :
subject = ' '
displayList . append ( ' %s . %s - %s - < %s >: %s ' % ( blockCount , blockDate , senderDisplay [ : 12 ] , subject [ : 10 ] , blockHash ) )
2018-12-09 17:29:39 +00:00
while choice not in ( ' -q ' , ' q ' , ' quit ' ) :
2018-10-02 16:45:56 +00:00
for i in displayList :
2019-06-20 00:59:05 +00:00
logger . info ( i , terminal = True )
2018-07-18 07:33:23 +00:00
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 :
2018-08-18 19:38:15 +00:00
cancel = ' '
2018-07-18 07:33:23 +00:00
readBlock . verifySig ( )
2019-06-26 00:15:04 +00:00
senderDisplay = bytesconverter . bytes_to_str ( readBlock . signer )
2019-02-05 18:47:11 +00:00
if len ( senderDisplay . strip ( ) ) == 0 :
senderDisplay = ' Anonymous '
2019-06-20 00:59:05 +00:00
logger . info ( ' Message received from %s ' % ( senderDisplay , ) , terminal = True )
logger . info ( ' Valid signature: %s ' % readBlock . validSig , terminal = True )
2018-11-17 07:23:10 +00:00
2018-07-19 07:08:51 +00:00
if not readBlock . validSig :
2019-06-20 00:59:05 +00:00
logger . warn ( ' This message has an INVALID/NO signature. ANYONE could have sent this message. ' , terminal = True )
2018-08-18 04:42:30 +00:00
cancel = logger . readline ( ' Press enter to continue to message, or -q to not open the message (recommended). ' )
2019-02-17 20:44:51 +00:00
print ( ' ' )
2018-08-18 04:42:30 +00:00
if cancel != ' -q ' :
2019-02-02 23:10:04 +00:00
try :
2019-06-25 23:07:35 +00:00
print ( draw_border ( escapeansi . escape_ANSI ( readBlock . bcontent . decode ( ) . strip ( ) ) ) )
2019-02-02 23:10:04 +00:00
except ValueError :
2019-06-20 00:59:05 +00:00
logger . warn ( ' Error presenting message. This is usually due to a malformed or blank message. ' , terminal = True )
2019-02-02 23:10:04 +00:00
pass
2019-02-17 20:44:51 +00:00
if readBlock . validSig :
reply = logger . readline ( " Press enter to continue, or enter %s to reply " % ( " -r " , ) )
print ( ' ' )
if reply == " -r " :
2019-06-26 00:15:04 +00:00
self . draft_message ( bytesconverter . bytes_to_str ( readBlock . signer , ) )
2019-02-17 20:44:51 +00:00
else :
logger . readline ( " Press enter to continue " )
print ( ' ' )
2018-07-16 07:40:58 +00:00
return
2018-11-11 03:25:40 +00:00
2018-11-01 04:56:59 +00:00
def sentbox ( self ) :
'''
Display sent mail messages
'''
entering = True
while entering :
2019-02-08 18:53:28 +00:00
self . get_sent_list ( )
2019-06-20 00:59:05 +00:00
logger . info ( ' Enter a block number or -q to return ' , terminal = True )
2018-11-01 04:56:59 +00:00
try :
2018-11-03 03:24:14 +00:00
choice = input ( ' > ' )
except ( EOFError , KeyboardInterrupt ) as e :
2018-11-01 04:56:59 +00:00
entering = False
else :
2018-12-09 17:29:39 +00:00
try :
choice = int ( choice ) - 1
except ValueError :
pass
2018-11-01 04:56:59 +00:00
else :
2018-11-03 03:24:14 +00:00
try :
2018-12-09 17:29:39 +00:00
self . sentboxList [ int ( choice ) ]
except ( IndexError , ValueError ) as e :
2019-06-20 00:59:05 +00:00
logger . warn ( ' Invalid block. ' , terminal = True )
2018-11-03 03:24:14 +00:00
else :
2019-06-20 00:59:05 +00:00
logger . info ( ' Sent to: ' + self . sentMessages [ self . sentboxList [ int ( choice ) ] ] [ 1 ] , terminal = True )
2018-11-03 03:24:14 +00:00
# Print ansi escaped sent message
2019-06-25 23:07:35 +00:00
logger . info ( escapeansi . escape_ANSI ( self . sentMessages [ self . sentboxList [ int ( choice ) ] ] [ 0 ] ) , terminal = True )
2018-11-03 03:24:14 +00:00
input ( ' Press enter to continue... ' )
2018-12-09 17:29:39 +00:00
finally :
if choice == ' -q ' :
entering = False
2018-11-01 04:56:59 +00:00
return
2018-11-11 03:25:40 +00:00
2019-02-08 18:53:28 +00:00
def get_sent_list ( self , display = True ) :
2018-11-03 03:24:14 +00:00
count = 1
2018-12-09 17:29:39 +00:00
self . sentboxList = [ ]
self . sentMessages = { }
2018-11-03 03:24:14 +00:00
for i in self . sentboxTools . listSent ( ) :
self . sentboxList . append ( i [ ' hash ' ] )
2019-06-26 00:15:04 +00:00
self . sentMessages [ i [ ' hash ' ] ] = ( bytesconverter . bytes_to_str ( i [ ' message ' ] ) , i [ ' peer ' ] , i [ ' subject ' ] )
2019-02-08 18:53:28 +00:00
if display :
2019-06-20 00:59:05 +00:00
logger . info ( ' %s . %s - %s - ( %s ) - %s ' % ( count , i [ ' hash ' ] , i [ ' peer ' ] [ : 12 ] , i [ ' subject ' ] , i [ ' date ' ] ) , terminal = True )
2018-11-03 03:24:14 +00:00
count + = 1
2019-02-08 18:53:28 +00:00
return json . dumps ( self . sentMessages )
2018-11-01 04:56:59 +00:00
2019-02-08 18:53:28 +00:00
def draft_message ( self , recip = ' ' ) :
2018-07-16 07:40:58 +00:00
message = ' '
newLine = ' '
2019-02-01 19:55:59 +00:00
subject = ' '
2018-12-09 17:29:39 +00:00
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
2019-06-25 08:21:36 +00:00
if not stringvalidators . validate_pub_key ( recip ) :
2018-12-09 17:29:39 +00:00
raise onionrexceptions . InvalidPubkey ( ' Must be a valid ed25519 base32 encoded public key ' )
except onionrexceptions . InvalidPubkey :
2019-06-20 00:59:05 +00:00
logger . warn ( ' Invalid public key ' , terminal = True )
2018-12-09 17:29:39 +00:00
except ( KeyboardInterrupt , EOFError ) :
entering = False
else :
break
2018-07-16 07:40:58 +00:00
else :
2018-12-09 17:29:39 +00:00
# if -q or ctrl-c/d, exit function here, otherwise we successfully got the public key
return
2019-02-01 19:55:59 +00:00
try :
subject = logger . readline ( ' Message subject: ' )
except ( KeyboardInterrupt , EOFError ) :
pass
2019-01-22 03:29:29 +00:00
cancelEnter = False
2019-06-20 00:59:05 +00:00
logger . info ( ' Enter your message, stop by entering -q on a new line. -c to cancel ' , terminal = True )
2018-07-16 07:40:58 +00:00
while newLine != ' -q ' :
try :
newLine = input ( )
except ( KeyboardInterrupt , EOFError ) :
2019-01-22 03:29:29 +00:00
cancelEnter = True
if newLine == ' -c ' :
cancelEnter = True
break
2018-07-16 07:40:58 +00:00
if newLine == ' -q ' :
continue
newLine + = ' \n '
message + = newLine
2018-07-17 07:18:17 +00:00
2019-01-22 03:29:29 +00:00
if not cancelEnter :
2019-06-20 00:59:05 +00:00
logger . info ( ' Inserting encrypted message as Onionr block.... ' , terminal = True )
2018-07-16 07:40:58 +00:00
2019-02-05 18:47:11 +00:00
blockID = self . myCore . insertBlock ( message , header = ' pm ' , encryptType = ' asym ' , asymPeer = recip , sign = self . doSigs , meta = { ' subject ' : subject } )
2019-02-08 18:53:28 +00:00
def toggle_signing ( self ) :
2019-02-05 18:47:11 +00:00
self . doSigs = not self . doSigs
2018-07-16 07:40:58 +00:00
def menu ( self ) :
choice = ' '
while True :
2019-02-05 18:47:11 +00:00
sigMsg = ' Message Signing: %s '
2018-07-16 07:40:58 +00:00
2019-06-20 00:59:05 +00:00
logger . info ( self . strings . programTag + ' \n \n User ID: ' + self . myCore . _crypto . pubKey , terminal = True )
2019-02-05 18:47:11 +00:00
if self . doSigs :
sigMsg = sigMsg % ( ' enabled ' , )
else :
sigMsg = sigMsg % ( ' disabled (Your messages cannot be trusted) ' , )
if self . doSigs :
2019-06-20 00:59:05 +00:00
logger . info ( sigMsg , terminal = True )
2019-02-05 18:47:11 +00:00
else :
2019-06-20 00:59:05 +00:00
logger . warn ( sigMsg , terminal = True )
logger . info ( self . strings . mainMenu . title ( ) , terminal = True ) # print out main menu
2018-07-16 07:40:58 +00:00
try :
2018-07-17 07:18:17 +00:00
choice = logger . readline ( ' Enter 1- %s : \n ' % ( len ( self . strings . mainMenuChoices ) ) ) . lower ( ) . strip ( )
2018-07-16 07:40:58 +00:00
except ( KeyboardInterrupt , EOFError ) :
choice = ' 5 '
if choice in ( self . strings . mainMenuChoices [ 0 ] , ' 1 ' ) :
self . inbox ( )
2018-07-19 07:08:51 +00:00
elif choice in ( self . strings . mainMenuChoices [ 1 ] , ' 2 ' ) :
2018-11-01 04:56:59 +00:00
self . sentbox ( )
2018-07-16 07:40:58 +00:00
elif choice in ( self . strings . mainMenuChoices [ 2 ] , ' 3 ' ) :
2019-02-08 18:53:28 +00:00
self . draft_message ( )
2018-07-19 07:08:51 +00:00
elif choice in ( self . strings . mainMenuChoices [ 3 ] , ' 4 ' ) :
2019-02-08 18:53:28 +00:00
self . toggle_signing ( )
2019-02-05 18:47:11 +00:00
elif choice in ( self . strings . mainMenuChoices [ 4 ] , ' 5 ' ) :
2019-06-20 00:59:05 +00:00
logger . info ( ' Goodbye. ' , terminal = True )
2018-07-16 07:40:58 +00:00
break
2018-07-17 07:18:17 +00:00
elif choice == ' ' :
pass
2018-07-16 07:40:58 +00:00
else :
2019-06-20 00:59:05 +00:00
logger . warn ( ' Invalid choice. ' , terminal = True )
2018-07-16 07:40:58 +00:00
return
2019-03-02 06:22:59 +00:00
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 ) )
2019-02-10 18:43:45 +00:00
def on_insertblock ( api , data = { } ) :
2019-02-11 22:36:43 +00:00
meta = json . loads ( data [ ' meta ' ] )
2019-06-16 22:34:43 +00:00
if meta [ ' type ' ] == ' pm ' :
sentboxTools = sentboxdb . SentBox ( api . get_core ( ) )
sentboxTools . addToSent ( data [ ' hash ' ] , data [ ' peer ' ] , data [ ' content ' ] , meta [ ' subject ' ] )
2019-02-10 18:43:45 +00:00
2018-07-16 07:40:58 +00:00
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 ' )
2018-11-11 03:25:40 +00:00
return