1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 import os
20 import sys
21
22 __version__ = '0.3.5'
23
24 __all__ = (
25 'serverline',
26 'SENDMAIL',
27 'validchars',
28 'alphanums',
29 'getrequest',
30 'getform',
31 'getall',
32 'isblank',
33 'formencode',
34 'formdecode',
35 'mailme',
36 'sendmailme',
37 'createhtmlmail',
38 'environdata',
39 'validemail',
40 'cgiprint',
41 'ucgiprint',
42 'replace',
43 'error',
44 'makeindexline',
45 'istrue',
46 'randomstring',
47 'blacklisted',
48 '__version__',
49 )
50
51 serverline = "Content-Type: text/html"
52
53
54 SENDMAIL = "/usr/sbin/sendmail"
55 validchars = 'abcdefghijklmnopqrstuvwxyz0123456789!-_*'
56 alphanums = 'abcdefghijklmnopqrstuvwxyz0123456789'
57
58
59
60
62 """
63 Initialise the ``FieldStorage`` and return the specified list of values as
64 a dictionary.
65
66 If you don't specify a list of values, then *all* values will be returned.
67
68 If you set ``nolist`` to ``True`` then any parameters supplied as lists
69 will only have their first entry returned.
70 """
71 import cgi
72 form = cgi.FieldStorage()
73 if valuelist is not None:
74 return getform(valuelist, form, nolist=nolist)
75 else:
76 return getall(form, nolist=nolist)
77
79 """
80 This function, given a CGI form, extracts the data from it, based on
81 valuelist passed in. Any non-present values are set to '' - although this
82 can be changed.
83
84 It also takes a keyword argument 'nolist'. If this is True list values only
85 return their first value.
86
87 Returns a dictionary.
88 """
89 data = {}
90 for field in valuelist:
91 if not theform.has_key(field):
92 data[field] = notpresent
93 else:
94 if not isinstance(theform[field], list):
95 data[field] = theform[field].value
96 else:
97 if not nolist:
98
99 data[field] = [x.value for x in theform[field]]
100 else:
101
102 data[field] = theform[field][0].value
103 return data
104
105 -def getall(theform, nolist=False):
106 """
107 Passed a form (FieldStorage instance) return all the values.
108 This doesn't take into account file uploads.
109
110 Also accepts the 'nolist' keyword argument as ``getform``.
111
112 Returns a dictionary.
113 """
114 data = {}
115 for field in theform.keys():
116 if not isinstance(theform[field], list):
117 data[field] = theform[field].value
118 else:
119 if not nolist:
120
121 data[field] = [x.value for x in theform[field]]
122 else:
123
124 data[field] = theform[field][0].value
125 return data
126
128 """
129 Passed an indict of values it checks if any of the values are set.
130
131 Returns ``True`` if every member of the indict is empty (evaluates as False).
132
133 I use it on a form processed with getform to tell if my CGI has been
134 activated without any values.
135 """
136 return not [val for val in indict.values() if val]
137
146
148 """Decode a single string back into a form like dictionary."""
149 from cgi import parse_qs
150 from urllib import unquote_plus
151 return parse_qs(unquote_plus(thestring), True)
152
153
154
155
156
157
158
159
160 -def mailme(to_email, msg, email_subject=None, from_email=None,
161 host='localhost', port=25, username=None, password=None,
162 html=True, sendmail=None):
163 """
164 This function will send an email using ``sendmail`` or ``smtplib``, depending
165 on what parameters you pass it.
166
167 If you want to use ``sendmail`` to send the email then set
168 ``sendmail='/path/to/sendmail'``. (The ``SENDMAIL`` value from Constants_ often
169 works).
170
171 If you aren't using sendmail then you will need to set ``host`` and ``port`` to
172 the correct values. If your server requires authentication then you'll need to
173 supply the correct ``username`` and ``password``.
174
175 ``to_email`` can be a single email address, *or* a list of addresses.
176
177 ``mailme`` *assumes* you are sending an html email created by
178 ``createhtmlmail``. If this isn't the case then set ``html=False``.
179
180 Some servers won't let you send a message without supplying a ``from_email``.
181 """
182 if sendmail is not None:
183
184 return sendmailme(to_email, msg, email_subject, from_email,
185 html, sendmail)
186 if not isinstance(to_email, list):
187
188 to_email = [to_email]
189
190 import smtplib
191
192 head = "To: %s\r\n" % ','.join(to_email)
193 if from_email is not None:
194 head += ('From: %s\r\n' % from_email)
195
196 if not html and email_subject is not None:
197 head += ("Subject: %s\r\n\r\n" % email_subject)
198 msg = head + msg
199
200 server = smtplib.SMTP(host, port)
201 if username:
202 server.login(username, password)
203 server.sendmail(from_email, to_email, msg)
204 server.quit()
205
206
207 -def sendmailme(to_email, msg, email_subject=None, from_email=None,
208 html=True, sendmail=SENDMAIL):
209 """
210 Quick and dirty, pipe a message to sendmail. Can only work on UNIX type systems
211 with sendmail.
212
213 Will need the path to sendmail - defaults to the 'SENDMAIL' constant.
214
215 ``to_email`` can be a single email address, *or* a list of addresses.
216
217 *Assumes* you are sending an html email created by ``createhtmlmail``. If this
218 isn't the case then set ``html=False``.
219 """
220 if not isinstance(to_email, list):
221 to_email = [to_email]
222 o = os.popen("%s -t" % sendmail,"w")
223 o.write("To: %s\r\n" % ','.join(to_email))
224 if from_email:
225 o.write("From: %s\r\n" % from_email)
226 if not html and email_subject:
227 o.write("Subject: %s\r\n" % email_subject)
228 o.write("\r\n")
229 o.write("%s\r\n" % msg)
230 o.close()
231
233 """
234 Create a mime-message that will render as HTML or text as appropriate.
235 If no text is supplied we use htmllib to guess a text rendering.
236 (so html needs to be well formed)
237
238 Adapted from recipe 13.5 from Python Cookbook 2
239 """
240 import MimeWriter, mimetools, StringIO
241 if text is None:
242
243 import htmllib
244 import formatter
245 textout = StringIO.StringIO()
246 formtext = formatter.AbstractFormatter(formatter.DumbWriter(textout))
247 parser = htmllib.HTMLParser(formtext)
248 parser.feed(html)
249 parser.close()
250 text = textout.getvalue()
251 del textout, formtext, parser
252 out = StringIO.StringIO()
253 htmlin = StringIO.StringIO(html)
254 txtin = StringIO.StringIO(text)
255 writer = MimeWriter.MimeWriter(out)
256
257
258 writer.addheader("Subject", subject)
259 writer.addheader("MIME-Version", "1.0")
260
261
262 writer.startmultipartbody("alternative")
263 writer.flushheaders()
264
265 subpart = writer.nextpart()
266 pout = subpart.startbody("text/plain", [("charset", 'iso-8859-l')])
267 pout.write(txtin.read())
268 txtin.close()
269
270 subpart = writer.nextpart()
271 subpart.addheader("Content-Transfer-Encoding", "quoted-printable")
272 pout = subpart.startbody("text/html", [("charset", 'us-ascii')])
273 mimetools.encode(htmlin, pout, 'quoted-printable')
274 htmlin.close()
275
276 writer.lastpart()
277 msg = out.getvalue()
278 out.close()
279 return msg
280
282 """Returns some data about the CGI environment, in a way that can be mailed."""
283 ENVIRONLIST = [ 'REQUEST_URI','HTTP_USER_AGENT','REMOTE_ADDR','HTTP_FROM','REMOTE_HOST','REMOTE_PORT','SERVER_SOFTWARE','HTTP_REFERER','REMOTE_IDENT','REMOTE_USER','QUERY_STRING','DATE_LOCAL' ]
284 environs = []
285 environs.append("\n\n---------------------------------------\n")
286 for x in ENVIRONLIST:
287 if os.environ.has_key(x):
288 environs.append("%s: %s\n" % (x, os.environ[x]))
289 environs.append("---------------------------------------\n")
290 return ''.join(environs)
291
293 """
294 A quick function to do a basic email validation.
295 Returns False or the email address.
296 """
297 if ' ' in email:
298 return False
299 dot = email.rfind('.')
300 at = email.find('@')
301 if dot == -1 or at < 1 or at > dot:
302 return False
303 return email
304
305
306
308 """The generic error function."""
309 print serverline
310 print
311 print '''<html><head><title>An Error Has Occurred</title>
312 <body><center>
313 <h1>Very Sorry</h1>
314 <h2>An Error Has Occurred</h2>'''
315 if errorval:
316 print '<h3>%s</h3>' % errorval
317 print '</center></body></html>'
318 sys.exit()
319
320
321
322 -def makeindexline(url, startpage, total, numonpage=10, pagesonscreen=5):
323 """
324 Make a menu line for a given number of inputs, with a certain number per page.
325 Will look something like : ::
326
327 First Previous 22 23 24 25 26 27 28 29 30 31 32 Next Last
328
329 Each number or word will be a link to the relevant page.
330
331 url should be in the format : ``'<a href="script.py?start=%s">%s</a>'`` -
332 it will have the two ``%s`` values filled in by the function.
333
334 The url will automatically be put between ``<strong></strong>`` tags. Your
335 script needs to accepts a parameter ``start`` telling it which page to
336 display.
337
338 ``startpage`` is the page actually being viewed - which won't be a link.
339
340 ``total`` is the number of total inputs.
341
342 ``numonpage`` is the number of inputs per page - this tells makeindexline how
343 many pages to divide the total into.
344
345 The links shown will be some before startpage and some after. The amount of
346 pages links are shown for is ``pagesonscreen``. (The actual total number shown
347 will be *2 \* pagesonscreen + 1*).
348
349 The indexes generated are *a bit* like the ones created by google. Unlike
350 google however, next and previous jump you into the *middle* of the next set of
351 links. i.e. If you are on page 27 next will take you to 33 and previous to 21.
352 (assuming pagesonscreen is 5). This makes it possible to jump more quickly
353 through a lot of links. Also - the current page will always be in the center of
354 the index. (So you never *need* Next just to get to the next page).
355 """
356 b = '<strong>%s</strong>'
357 url = b % url
358 outlist = []
359 last = ''
360 next = ''
361 numpages = total//numonpage
362 if total%numonpage:
363 numpages += 1
364 if startpage - pagesonscreen > 1:
365 outlist.append(url % (1, 'First'))
366 outlist.append(' ')
367 outlist.append(url % (startpage-pagesonscreen-1, 'Previous'))
368 outlist.append(' ')
369 index = max(startpage - pagesonscreen, 1)
370 end = min(startpage+pagesonscreen, numpages)
371 while index <= end:
372 if index == startpage:
373 outlist.append(b % startpage)
374 else:
375 outlist.append(url % (index, index))
376 index += 1
377 outlist.append(' ')
378 if (startpage+pagesonscreen) < numpages:
379 outlist.append(url % (startpage+pagesonscreen+1, 'Next'))
380 outlist.append(' ')
381 outlist.append(url % (numpages, 'Last'))
382
383 return ' '.join(outlist)
384
385
386
388 """
389 Accepts a string as input.
390
391 If the string is one of ``True``, ``On``, ``Yes``, or ``1`` it returns
392 ``True``.
393
394 If the string is one of ``False``, ``Off``, ``No``, or ``0`` it returns
395 ``False``.
396
397 ``istrue`` is not case sensitive.
398
399 Any other input will raise a ``KeyError``.
400 """
401 return {
402 'yes': True, 'no': False,
403 'on': True, 'off': False,
404 '1': True, '0': False,
405 'true': True, 'false': False,
406 }[value.lower()]
407
409 """
410 Return a random string of length 'length'.
411
412 The string is comprised only of numbers and lowercase letters.
413 """
414 import random
415 outstring = []
416 while length > 0:
417 length -= 1
418 outstring.append(alphanums[int(random.random()*36)])
419 return ''.join(outstring)
420
421
422
423 -def cgiprint(inline='', unbuff=False, line_end='\r\n'):
424 """
425 Print to the ``stdout``.
426
427 Set ``unbuff=True`` to flush the buffer after every write.
428
429 It prints the inline you send it, followed by the ``line_end``. By default this
430 is ``\r\n`` - which is the standard specified by the RFC for http headers.
431 """
432 sys.stdout.write(inline)
433 sys.stdout.write(line_end)
434 if unbuff:
435 sys.stdout.flush()
436
437 -def ucgiprint(inline='', unbuff=False, encoding='UTF-8', line_end='\r\n'):
438 """
439 A unicode version of ``cgiprint``. It allows you to store everything in your
440 script as unicode and just do your encoding in one place.
441
442 Print to the ``stdout``.
443
444 Set ``unbuff=True`` to flush the buffer after every write.
445
446 It prints the inline you send it, followed by the ``line_end``. By default this
447 is ``\r\n`` - which is the standard specified by the RFC for http headers.
448
449 ``inline`` should be a unicode string.
450
451 ``encoding`` is the encoding used to encode ``inline`` to a byte-string. It
452 defaults to ``UTF-8``, set it to ``None`` if you pass in ``inline`` as a byte
453 string rather than a unicode string.
454 """
455 if encoding:
456 inline = inline.encode(encoding)
457
458 sys.stdout.write(inline)
459 sys.stdout.write(line_end)
460 if unbuff:
461 sys.stdout.flush()
462
464 """
465 This function provides a simple but effective template system for your html
466 pages. Effectively it is a convenient way of doing multiple replaces in a
467 single string.
468
469 Takes a string and a dictionary of replacements.
470
471 This function goes through the string and replaces every occurrence of every
472 dicitionary key with it's value.
473
474 ``indict`` can also be a list of tuples instead of a dictionary (or anything
475 accepted by the dict function).
476 """
477 indict = dict(indict)
478 if len(indict) > 40:
479 regex = re.compile("(%s)" % "|".join(map(re.escape, indict.keys())))
480
481 return regex.sub(lambda mo: indict[mo.string[mo.start():mo.end()]],
482 instring)
483 for key in indict:
484 instring = instring.replace(key, indict[key])
485 return instring
486
487
488
489 -def blacklisted(ip, DNSBL_HOST='sbl-xbl.spamhaus.org'):
490 """
491 Returns ``True`` if ip address is a blacklisted IP (i.e. from a spammer).
492
493 ip can also be a domain name - this raises ``socket.gaierror`` if the ip is
494 a domain name that cannot be resolved.
495
496 The DNS blacklist host (``DNSBL_HOST``) defaults to *sbl-xbl.spamhaus.org*.
497
498 Other ones you could use include :
499
500 - 'relays.ordb.org'
501 - 'dns.rfc-ignorant.org'
502 - 'postmaster.rfc-ignorant.org'
503 - 'http.dnsbl.sorbs.net'
504 - 'misc.dnsbl.sorbs.net'
505 - 'spam.dnsbl.sorbs.net'
506 - 'bl.spamcop.net'
507
508 Useful for vetting user added information posted to web applications.
509
510 - 'relays.ordb.org'
511 - 'dns.rfc-ignorant.org'
512 - 'postmaster.rfc-ignorant.org'
513 - 'http.dnsbl.sorbs.net'
514 - 'misc.dnsbl.sorbs.net'
515 - 'spam.dnsbl.sorbs.net'
516 - 'bl.spamcop.net'
517
518 .. note::
519
520 Another, possibly more effective, way of coping with spam input to web
521 applications is to use the `Akismet Web Service <http://akismet.com>`_.
522
523 For this you can use the
524 `Python Akismet API Interface <http://www.voidspace.org.uk/python/modules.shtml#akismet>`_.
525 """
526
527 import socket
528
529
530 ip = socket.gethostbyname(ip)
531 iplist = ip.split('.')
532 iplist.reverse()
533 ip = '%s.%s' % ('.'.join(iplist), DNSBL_HOST)
534 try:
535 socket.gethostbyname(ip)
536 return True
537 except socket.gaierror:
538 return False
539
540
541
542 if __name__ == '__main__':
543 print 'No tests yet - sorry'
544
545 """
546 TODO/ISSUES
547 ===========
548
549 The indexes generated by makeindexline use next to jump 10 pages. This is
550 different to what people will expect if they are used to the 'Google' type
551 index lines.
552
553 createhtmlmail assumes iso-8859-1 input encoding for the html
554
555 email functions to support 'cc' and 'bcc'
556
557 Need doctests
558
559 Changelog
560 =========
561
562 2005/11/26 Version 0.3.5
563 -----------------------------
564
565 Add the ``blacklisted`` function.
566
567 Added ``__version__``
568
569
570 2005/10/29 Version 0.3.4
571 -----------------------------
572
573 Shortened ``isblank``.
574
575
576 2005/09/21 Version 0.3.3
577 -----------------------------
578
579 Fixed bug in ``getall``.
580
581 Fixed bug in ``getrequest``.
582
583
584 2005/08/27 Version 0.3.2
585 -----------------------------
586
587 Large dictionary replaces use a regex approach.
588
589
590 2005/08/20 Version 0.3.1
591 -----------------------------
592
593 Improved istrue function.
594
595 Added __all__.
596
597 Various other code/doc improvements.
598
599
600 2005/04/07 Version 0.3.0
601 -----------------------------
602
603 Changed the email functions, this may break things (but it's better this way)
604
605 Added createhtmlemail, removed loginmailme
606
607 mailme is now a wrapper for sendmailme, mailme, *and* the old loginmailme
608
609
610 2005/03/20 Version 0.2.0
611 -----------------------------
612
613 Added ucgiprint and replace.
614
615
616 2005/02/18 Version 0.1.0
617 -----------------------------
618
619 The first numbered version.
620 """
621