/
mail2web.py
executable file
·639 lines (544 loc) · 22.4 KB
/
mail2web.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# file name: ''mail2web.py''
# project: eMail Web Interface
# function: Fetches web pages by a simple eMail interface
# - Runs over the .forward file in the linux home directory
#
# Content of ~/.forward:
# |"cd /home/username/mail2web; FAKECHROOT_EXCLUDE_PATH=/usr/lib/python2.5 fakechroot ./watcher_report_output.sh 2>&1 >> watcher_report_output.log"
# created: 2010-10-04
# last change: $LastChangedRevision$
#
# Copyright (C) 2010 Robert Lange <sd2k9@sethdepot.org>
# 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; version 2 of the License
#
# 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# *** Import modules
# To control output level easily
# from __future__ import print_function
import logging
# Argument parser
import optparse
# Read from stdin, Program path
import sys
# Prepare a Message as E-Mail
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# from email.encoders import encode_quopri
from email import Charset
# Send mail by SMTP
import smtplib
# To be able to catch also socket errors by their name
import socket
# URL Fetcher
import urllib
# Current date for Train fetcher
import datetime
# Chroot ability
import os
# Get the list of allowed users accessing this service
from mail2webuser import UserList
# *** Global settings as dictionary
Opts = {
# Sender of the messages
'sender': 'mail2web@localhost',
# SMTP Server, Host
'smtp_server_host': '127.0.0.1',
'smtp_server_port': '25', # For Direct Sending
# Limit of data to fetch: 100k
'data_limit': 100*1024
}
# Provide logging shortcuts
pinfo = logging.info
pwarn = logging.warning
perror = logging.error
# ********************************************************************************
# *** Go into CHROOT - see help text in except block
chroot_dir = sys.path[0] + "/empty_chroot"
try:
os.chroot(chroot_dir)
pass
except OSError:
# Something failed with CHROOTing - give help text
print """
Going into chroot failed; did you forgot to run
me with fakechroot?
To succeed the python libs must be excluded from CHROOT'ing
with the shell variable setting
FAKECHROOT_EXCLUDE_PATH=/usr/lib/python<version>"""
print " Chroot directory: " + chroot_dir
print
raise # and now just propagate the error further
# ********************************************************************************
# *** Command execution objects
class WorkerBase:
"""Base class for command execution classes."""
def execute(self,args):
"""Perform the command with the supplied arguments.
Parameter args: The argument string for the respective command
Return: Webpage to attach to the mail to send as result (as string)
None when no Mail should be send
"""
# Don't call the base class by itself
NotImplementedError("Abstract base class must not be called!")
class WorkerTest(WorkerBase):
"""Just returns a Hello World message."""
def execute(self,args):
pinfo("Test: Returning Hello World Document")
return "<html>Hello World</html>"
class WorkerWebFetch(WorkerBase):
"""Fetches the given address and returns it."""
def execute(self,args):
# When no URL scheme specified, just expect "http://"
if args.find("://") is -1 :
url = "http://" + args
else:
url = args
pinfo(" Web: Fetching " + url + " (Limit: " + str(Opts['data_limit']) + ")" )
try:
url = urllib.urlopen(url)
data = url.read(Opts['data_limit'])
# urllib2: except (urllib.URLError, ValueError) as detail:
except (IOError, ValueError), detail:
# Showing error message as return
return "<html>Error fetching data from " + args + ":</br>" + \
str(detail) + "</html>"
else: # No error, return the data fetched
return data;
class MozillaUrlOpener(urllib.FancyURLopener):
"""For getting access to google, we need to change the user agent
That is working the following way: http://wolfprojects.altervista.org/changeua.php
"""
version = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11'
class WorkerGoogle(WorkerBase):
"""Queries Google with the supplied search term."""
def execute(self,args):
urlarg = urllib.quote_plus(args) # Encode the data
pinfo(" Google: Fetching Data " + urlarg + " (Limit: " + str(Opts['data_limit']) + ")" )
# Template: http://www.google.co.jp/search?q=hallo&safe=off
urldata = "http://www.google.com/search?q=%s&safe=off" % (urlarg)
try:
url = MozillaUrlOpener()
url_open = url.open(urldata)
data = url_open.read(Opts['data_limit'])
# urllib2: except (urllib.URLError, ValueError) as detail:
except (IOError, ValueError), detail:
# Showing error message as return
return "<html>Error fetching data from " + args + ":</br>" + \
str(detail) + "</html>"
else: # No error, return the data fetched
return data;
class WorkerGoogleFirst(WorkerBase):
"""Returns the first match from Google with the supplied search term.
<br />
This function is also known as "Feeling Lucky Search".
"""
def execute(self,args):
urlarg = urllib.quote_plus(args) # Encode the data
pinfo(" Google First: Fetching Data " + urlarg + " (Limit: " + str(Opts['data_limit']) + ")" )
# Template: http://www.google.co.jp/search?q=hallo&safe=off&btnI
urldata = "http://www.google.com/search?q=%s&safe=off&btnI" % (urlarg)
try:
url = MozillaUrlOpener()
url_open = url.open(urldata)
data = url_open.read(Opts['data_limit'])
# urllib2: except (urllib.URLError, ValueError) as detail:
except (IOError, ValueError), detail:
# Showing error message as return
return "<html>Error fetching data from " + args + ":</br>" + \
str(detail) + "</html>"
else: # No error, return the data fetched
return data;
class WorkerWadoku(WorkerBase):
"""Queries Wadoku German-Japanese dictionary."""
def execute(self,args):
urlarg = self.quote(args) # Encode the data and Umlauts
pinfo(" Wadoku: Asking for " + urlarg + " (Limit: " + str(Opts['data_limit']) + ")" )
# Template: http://www.wadoku.de/wadoku/search/hallo
urldata = "http://www.wadoku.de/wadoku/search/%s" % (urlarg)
try:
url = urllib.urlopen(urldata)
data = url.read(Opts['data_limit'])
# urllib2: except (urllib.URLError, ValueError) as detail:
except (IOError, ValueError), detail:
# Showing error message as return
return "<html>Error fetching data from " + args + ":</br>" + \
str(detail) + "</html>"
else: # No error, return the data fetched
return data;
def quote(self, args):
"""Quote the URL data and restore the Umlauts as required by Wadoku"""
data = urllib.quote_plus(args)
data = data.replace("Ae","%C3%84")
data = data.replace("Oe","%C3%96")
data = data.replace("Ue","%C3%9C")
data = data.replace("ae","%C3%A4")
data = data.replace("oe","%C3%B6")
data = data.replace("ue","%C3%BC")
data = data.replace("sz","%C3%9F")
return data
# Fahrplan-Abfrage mit http://world.jorudan.co.jp/norikae/cgi-bin/engkeyin.cgi
class WorkerNorikae(WorkerBase):
"""Queries Norikae train time table. <br />
We expect the format [TO.]FROM.TO[.HH.MM[.DD[.Month[.YYYY]]]]] <br />
Everything missing will be taken as now. <br />
String literal "TO." at beginning will select arrival-time instead of departure time.
<br />
Examples:<br />
DO train TO.Tokyo.Miyaji - Timetable now from Tokyo to Miyaji<br />
DO train TO.Tokyo.Miyaji.17.45.20.11 - Timetable from Tokyo to Miyaji on 5.45pm 20.Nov
<br />
Attention: When you misspell the stations you will get an meaningless error message!
"""
def execute(self,args):
url = "http://world.jorudan.co.jp/norikae/cgi-bin/engkeyin.cgi"
raw_data = self.parse_args(args) # parse arguments
data = urllib.urlencode(raw_data) # Encode for POST
if data is None:
pinfo(" Norikae: Failing to parse argument string " + args)
# Showing error message as return
return "<html>Train: Failing to parse argument string " + args + \
"</html>"
# print url # Debug
# print data # Debug
pinfo(" Norikae: Asking for " + data + " (Limit: " + str(Opts['data_limit']) + ")" )
# import sys # Debug
# sys.exit(1) # Debug
try:
url = urllib.urlopen(url, data) # Do a post request
data = url.read(Opts['data_limit'])
# urllib2: except (urllib.URLError, ValueError) as detail:
except (IOError, ValueError), detail:
# Showing error message as return
return "<html>Error fetching data from " + args + ":</br>" + \
str(detail) + "</html>"
else: # No error, return the data fetched
return data;
def parse_args(self, args_in):
"""Parse the arguments into dict usable with Norikae POST
Returns None for Fail
"""
# First we parse the arguments into abstract values
dc = self.parse_abstract_args(args_in)
if dc is None:
return None # Some failure occured
# Process to actual field names
# from_in: text
# to_in: text
# Dyyyymm - YYYYMM
# Ddd - DD
# Dhh - h am/pm
# Dmn - m
# Sfromto - from/to
dcp = {}
dcp['from_in'] = dc['from']
dcp['to_in'] = dc['to']
dcp['Dyyyymm'] = "%04d%02d" % (int(dc['year']), int(dc['month']))
dcp['Ddd'] = "%02d" % (int(dc['day']))
dcp['Dhh'] = "%02d" % (int(dc['hour']))
dcp['Dmn'] = "%02d" % (int(dc['min']))
dcp['Sfromto'] = dc['fromto']
# Preset values from my side
dcp['Sseat'] = "1" # Unreserved
# dcp['Bchange'] = "" # Button not clicked
dcp['Bsearch'] = " Search " # Search button clicked
# Other hidden values
dcp["Knorikae"] = "Knorikae"
dcp["proc_mode"] = "K"
dcp["proc_sw"] = "11" # 2nd screen
dcp["proc_sw_sub"] = "0"
dcp["from_nm"] = ""
dcp["to_nm"] = ""
dcp["Sfrom_sw"] = "1"
return dcp
def parse_abstract_args(self, args):
"""Parse the arguments into abstract dict.
Returns None for Fail
"""
# Dummy Debug function
# dc = {}
# dc['from'] = "Tatsutaguchi"
# dc['to'] = "Miyaji"
# dc['year'] = 2010
# dc['month'] = 11
# dc['day'] = 20
# dc['hour'] = 15
# dc['min'] = 40
# dc['fromto'] = 'from'
# Fill with today's defaults - adjust to Japan Time
now = datetime.datetime.utcnow() + datetime.timedelta(hours=9)
dc = {}
dc['year'] = now.year
dc['month'] = now.month
dc['day'] = now.day
dc['hour'] = now.hour
dc['min'] = now.minute
# We expect the format [TO.]FROM.TO.[.MM[.HH[.DD[.Month[.YYYY]]]]]
sa = args.split(".") # Just split on "."
# print sa # Debug
# Just parse it the way we're going
if not sa:
return None
data = sa.pop(0)
if data == "TO":
dc['fromto'] = 'to' # To-Mode
# Fetch also real from
if not sa:
return None
data = sa.pop(0)
else:
dc['fromto'] = 'from' # From-Mode
dc['from'] = data
if not sa:
return None
data = sa.pop(0)
dc['to'] = data
# Now all defaults are filled and we will exit when it's fine
if not sa:
return dc
data = sa.pop(0)
dc['hour'] = data
if not sa:
return dc
data = sa.pop(0)
dc['min'] = data
if not sa:
return dc
data = sa.pop(0)
dc['day'] = data
if not sa:
return dc
data = sa.pop(0)
dc['month'] = data
if not sa:
return dc
data = sa.pop(0)
dc['year'] = data
# All parsed and done
return dc
# *** Build dict of worker classes, with the command name as key
WorkerClasses = {'test': WorkerTest,
'web': WorkerWebFetch,
'google': WorkerGoogle,
'gfirst': WorkerGoogleFirst,
'wadoku': WorkerWadoku,
'train': WorkerNorikae,
}
# ********************************************************************************
# *** Functions
def extract_mail_content(readin):
"""Extract required information from mail streamed into stdin.
Also character encoding for command is transformed when required
Argument: file object to read from
Return: Sender E-Mail, Passcode, Command-String
"""
# Read the first 16k - to avoid any buffer overflow or so
data = readin.read(16*1024)
# Debug
# print data
# Variables
addr = ''
code = ''
command = ''
charset = ''
# Extract the data
for it in data.split("\n"):
if it.startswith("From: ") and addr is '' : # We found from, for 1st time
addr = it[6:]
if it.startswith("Subject: ") and code is '' : # We found code, for 1st time
code = it[9:]
if it.startswith("DO ") and command is '' : # We found cmd, for 1st time
command = it[3:]
if it.startswith("Content-Type:") and charset is '' : # We found character encoding?
charsetfind = it.rfind("charset=")
if charsetfind is not -1: # Yes, we have some
charset = it[charsetfind+8:]
# Okay, change character encoding for the command when specified
# But no need to recode UTF-8
if (charset is not '') and not (charset.lower() == "utf-8") :
pinfo("Recode command from charset " + charset)
try:
command_re = command.decode("ISO-2022-JP")
command_re = command_re.encode("utf-8")
command = command_re
except UnicodeError, details:
pwarning("Failed to recode command! Error is " +\
str(detail) +\
"\nContinuing with orginal string.")
# Return
return addr, code, command
def check_user(sender, passcode):
""" Check for valid user and password.
Returns 2 Parameters
1.P (valid): true when a valid user/password pair was found
2.P (receiver): Return mail address for this user
"""
# Return Vars
valid = False
rec = None
# Search for sender address
for it in UserList:
if sender.find(it['from']) is not -1: # We have a match
pinfo("Matched user " + it['from'] + " in " + sender)
if it['code'] == passcode: # And also the password is OK
pinfo(" Password matched")
rec = it['replyto']
if rec:
valid = True # Everything fine
else: # There is no replyto defined?!
perror(" Empty replyto fetched from user definition! You are wrong! Pleas fix")
break # Out of our loop
else:
pwarn(" Password verification failed, continuing with next user")
else: # Just give a status, this is an invalid mail
pinfo("No user matched; silenty ignoring mail")
# Just return our results
return valid, rec
def send_mail(command_id, command_raw, payload, rec):
"""Sent the fetched information as Mail to the receiver.
Parameter
command_id: Name of the executed command
command_raw: Complete command with arguments
payload: The HTML page to return
rec: The Mail receiver
Return: True for OK, False for Failure
"""
# Override python's weird assumption that utf-8 text should be encoded with base64
Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
# First format as a message
# Create the container (outer) email message.
msg = MIMEMultipart()
msg['Subject'] = 'Mail2Web Fetched: ' + command_id
# me == the sender's email address
msg['From'] = Opts['sender']
msg['To'] = rec
# Plain text as explanation
explanation = "This is the demon slave\n" + \
"I executed for you:\n" + command_raw + \
"\n\nHave fun!\n" + \
"To reach some admin, please mailto: admin@thishost\n" + \
"This mail address only takes commands for fetching web sites\n" +\
"\nPlease be also remindad that this is an beta service. " +\
"This means particularly that I am currently logging and " +\
"inspecting all accesses."
# This is they way also Softbank Phones understand it
msg_explanation = MIMEText(explanation, "plain", "utf-8" ) # And the explanation as text
msg_explanation.add_header('Content-Disposition', 'inline') # Show message inline
msg.attach(msg_explanation)
# And our HTML we want to send, and it's an attachment
msg_payload = MIMEText(payload, "html", "utf-8")
msg_payload.add_header('Content-Disposition', 'attachment; filename="result.html"')
msg.attach(msg_payload)
# Send the email via our own SMTP server.
# Debug
# print msg.as_string()
# return False
pinfo("Will send the result now to " + rec + " ...")
try: # Open Connection; Catch exceptions
smtp = smtplib.SMTP(Opts['smtp_server_host'], Opts['smtp_server_port'])
except (smtplib.SMTPException, socket.error), detail:
perror("Establishing SMTP connection to " + \
Opts['smtp_server_host'] + ":" + Opts['smtp_server_port'] + \
" failed: " + str(detail))
return False
try: # Send data; Catch exceptions
smtp.sendmail(Opts['sender'], rec, msg.as_string())
smtp.quit()
except smtplib.SMTPException, detail:
perror("SMTP Mail sending failed: " + str(detail))
return False
pinfo(" Successfully send " + str(len(msg.as_string())) + " bytes; Finished now\n")
# Everything fine - let's return
return True
# ********************************************************************************
# *** Main Program
def main():
# *** Command line parsing
cmd_usage="usage: %prog [options] args"
cmd_desc ="""Mail2Web - don't call yourself, add it into your .forward file"""
cmd_version="%prog " + __version__
cmd_parser = optparse.OptionParser(usage=cmd_usage,
description=cmd_desc, version=cmd_version)
cmd_parser.add_option('-V', action='version', help=optparse.SUPPRESS_HELP)
cmd_parser.add_option('--quiet', '-q', dest='quiet', action='store_true',
default=False, help='Quiet Output')
# more options to add
(cmd_opts, cmd_args) = cmd_parser.parse_args()
# Setup logging: Show only from warnings when being QUIET
logging.basicConfig(level=logging.WARNING if cmd_opts.quiet else logging.INFO,
format="%(message)s")
# Chroot logging only here, because otherwise it will confuse the logger
pinfo("Went into chroot with directory " + chroot_dir)
# Abort when different than no argument
if len(cmd_args) != 0:
perror("No command line arguments are expected!")
cmd_parser.print_usage()
return 1
# *** First read stdin to extract the information for parsing
sender, passcode, command_raw = extract_mail_content(sys.stdin)
# Error check: All must be filled
if not sender:
perror("Failed to identify sender name - something's wrong")
return 1
if not passcode:
perror("Failed to identify passcode - empty subject?")
return 1
if not command_raw:
perror("Failed to identify command - you forgot the command?")
return 1
# Break down the command string: "cmd aguments"
command_id, dummy, command_arg = command_raw.partition(" ")
# Debug
# print "from", sender
# print "code", passcode
# print "cmd", command_raw
# print "cmd_id", command_id
# print "cmd_arg", command_arg
# Check for User, and Password
valid, receiver = check_user(sender, passcode)
if not valid: # Check failed, so we go out here
# "No valid authentication, exiting" -> Should be reported in check_user
return 0
# DBG
# print receiver
# Check for command existence and call it
if command_id in WorkerClasses: # Found it
pinfo("Command: " + command_raw)
ret = (WorkerClasses[command_id])().execute(args=command_arg)
else: # Fake an return with help message
ret = "<html><p>Your command " + command_id + " is not supported.</p>"
ret += "<p>Supported commands are:<ul>"
for it,obj in WorkerClasses.iteritems():
ret += "<li>" + it + ":<br />" + obj.__doc__ + "</li>"
ret += "</ul></p></html>"
pinfo("Unknown command, send short help message instead")
# If it's None, then we're done here
if ret is None:
pinfo("Worker returned None; exiting w/o further actions")
return 0
# Something returned? Submit it
# Debug
# print ret
# return 0
# Send the result by email
if not send_mail(command_id=command_id, command_raw=command_raw,
payload=ret, rec=receiver):
return 1 # There was some kind of error
# All done, over and out
return 0
# *** Call Main program
__version__ = ''.join(filter(str.isdigit, "$LastChangedRevision$"))
if __version__ == "":
__version__ = "(development version)"
if __name__ == "__main__":
sys.exit(main())