2 #----------------------------------------------------------------------------
3 # Name: ErrorDialogs.py
5 # Created: September--October 2001
6 # Author: Chris Fama of Wholly Snakes Software,
7 # Chris.Fama@whollysnakes.com
8 #----------------------------------------------------------------------------
10 ErrorDialogs.py: by Christopher J. Fama (trading under the name
11 Wholly Snakes Software {Australian Business Number: 28379514278}).
13 This code is released under the auspices of the modified version of
14 the GNU Library Public License (LGPL) that constitutes the license
15 under which the GUI package wxPython is released [see the file
16 LICENSE.TXT accompanying that distribution). I must also thank Graham
17 Boyd, of Boyd Mining, and CEO of the Minserve group of companies
18 (www.minserve.com.au), for kindly allowing the release of this
19 module under a "free" license, despite a certain part of it's
20 development having taken place while working for him...
22 Please note that this code, written for Python 2.1, is derives from
23 code written when Python 1.5.2 was current. Although since Python 2.0
24 the sys.excepthook variable has been available, for ease and potential
25 backwards compatibility (or to be more realistic
26 backwards-PARTIAL-compatibility!), the code catches errors by
27 assigning a custom object to sys.stderr and monitoring calls to it's
28 write() method. Such calls which take place with a new value of
29 sys.last_traceback stand as evidence that at interpreter error has
30 just occurred; please note that this means that NO OTHER OUTPUT TO
31 sys.stderr WILL BE DISPLAYED. THIS INCLUDES "warnings" generated by
32 the interpreter. As far as I am aware, unless your code itself writes
33 to sys.stderr itself, these will be the only things you miss out
34 on--and many, if not most or all, of these will occur before your code
35 even gets the opportunity to set one of these file-like objects in
36 place. If this is a problem for you and you can't work around it,
37 please contact means or the wxPython-users mailing list.
42 This is a module to display errors--either explicitly requested by a
43 script or arbitrary interpreter errors--these needs arose in the
44 course of a commercial (contract) project, but were not developed in
45 the course of work on such [well, very little, albeit concurrently]...
46 [NBNB.Does not currently support display of other than interpreter errors.]
48 Usage, after 'from wxPython.lib.ErrorDialogs import *' (all
49 identifiers defined in this module begin with "wxPy", and many of them
50 with "wxPyError_", so there should be no namespace conflicts...):
52 wxPyNewErrorDialog (<win> (['frame='] <frame (can be None)>,
55 wxPyDestroyErrorDialogIfPresent () # e.g. when top frame destroyed)
57 for unhandled errors, or
59 returnval = wxpyNonFatalError (<frame (can be None)>,
62 [NOTE: NOT IMPLEMENTED {IN THIS VERSION} YET...]
64 returnval = wxPyFatalError (<HTML message>,
65 [,<OPTIONS, an extra one of which may be
66 'frame=' <frame (defaults to None)>>])
67 [NOTE: NOT IMPLEMENTED {IN THIS VERSION} YET...]
70 wxPybNonWindowingError (message)
75 wxPyNonFatalErrorDialog
77 wxPyFatalErrorDialogWithTraceback
78 wxPyNonFatalErrorDialogWithTraceback
79 wxPyNonWindowingErrorHandler
81 and the OPTIONS (with defaults) are: (please note that the options for
82 wxPyNonWindowingErrorHandler / wxPyNonWindowingError are 'almost' a (small))
85 'modal' [default 1]: block until dismissed.
87 'programname' [default "Python program"]: appears inThe
88 caption of the dialog, amidst it's text and (by default) in mailings.
90 'whendismissed' option, if given, this should be a string, to be
91 executed in a restricted environment (see rexec module) after
92 dialog dismissal. Typically, this should be Python code to "clean
93 up" after what was presumably the partial execution of some
94 operation started by the user, after the unexpected interruption
95 by some error [which--at least the way I work, means an unexpected
96 error in the code, since exceptions that may be raised by some
97 foreseen (in a programmatic sense) should normally be handled by
98 try...except clauses].
99 NOTE THAT CURRENTLY THE rexec CODE IS NOT WORKING, SO THIS IS JUST DONE
102 'mailto': if None, give no "e-mail support" option, otherwise this
103 is meant to be an address for 'bug reports'; a command button will
104 be created for this, which pops up another dialog [Linux], or a window of
105 another program [Windows].
107 On Windows, this will launch your mailer (assuming your mailer
108 would start when a file with the '.eml'extension is double-clicked
109 in Windows Explorer--this will be the case for Microsoft Outlook
110 Express [tm and all those legal necessities] if installed, and the
111 association has not been taken over by some other program. On
112 Linux, it will open a file dialog to save the message, by default
113 as a '.html' file...{Please note that, on Windows and with current
114 (when I got the machine I'm writing this on--early 2000) versions
115 of Outlook Express, you may need to enter your e-mail address in
116 the box beside the "mail support" button.)
118 The template for the mail message is in the 'MessageTemplate'
119 attribute of the dialog-derived class in question (e.g.,
120 wxPyNonFatalErrorDialog). Search for this in the code below to
121 see the default (note that this template is in HTML format). This
122 attributes uses the '%{name}s' string conversion feature of Python
123 [see sec.2 of the Python Library Reference]; allowed Lance are:
124 programname version exceptionname exceptionvalue
125 extraexceptioninformation traceback
127 'version': str(this) will appear after 'Version' below "Error in
128 <programname>" at the top of the dialog.
132 sys.stderr = wxPyNonFatalErrorWindowWithTraceback (
134 programname='sumthing',
136 whendismissed="from wxPython.wx import * ; wxBell()")
138 FOR INTERNATIONAL [NON-ENGLISH-SPEAKING] USE:
139 wxPyNonFatalErrorDialog and relatives have the method
140 SetText(Number NUMBER, STRING)
142 where STRING is to displayed in the wxStaticText control with ID
143 wxPyError_ID_TEXT<NUMBER>--see the automatically-generated code
144 for information about the meaning of these...
149 #_debug = 1 # uncomment to display some information (to stdout)
151 from wxPython
.wx
import *
152 import string
, sys
, traceback
, time
, rexec
, operator
, types
, tempfile
, os
153 #from wxPython.lib.createandsendHTMLmail import *# now inline
154 import MimeWriter
, mimetools
, cStringIO
, smtplib
156 from ErrorDialogs_wdr
import *
158 # You may see from the above line that I used the excellent RAD tool
159 # wxDesigner, by Robert Roebling, to accelerate development of this
160 # module... The above is left for the convenience of making future
161 # changes with wxDesigner; also so the wxDesigner-generated codedoes
162 # not need to precede the "hand-generated" code in this file; finally,
163 # as a personal endorsement: it is truly a brilliant time-saver!
164 # Please note that, at the time of writing, the wxDesigner-generated
165 # output requires manual removal of the PythonBitmaps function--an
166 # appropriate version of this function will be imported from a
167 # similarly- named module. Another manual change will have to be made
168 # to the automatically-generated source: "parent.sizerAroundText = "
169 # should be added [immediately] before the text similar to "item13 =
170 # wxStaticBoxSizer( item14, wxVERTICAL )", this sizer being the one
171 # containing the wxTextCtrl... [IMPORTANT NOTE: THIS LINE SHOULD BE
172 # THE ONE INSTANTIATING A wxStaticBoxSizer, *NOT* THE wxStaticBox
175 def wxPyDestroyErrorDialogIfPresent():
176 if isinstance(sys
.stderr
,wxPyNonFatalErrorDialog
):
180 def wxPyNewErrorDialog(dlg
):
181 wxPyDestroyErrorDialogIfPresent()
184 class wxPyNonWindowingErrorHandler
:
187 def __init__(self
,fatal
=0,file=sys
.__stderr
__):
192 if string
.find(s
,"Warning") <> 0\
193 and self
.this_exception
is not sys
.last_traceback
:
194 wxPyNonWindowingError("The Python interpreter encountered an error "
195 "not handled by any\nexception handler--this "
196 "may represent some programming error.",
200 self
.this_exception
= sys
.last_traceback
202 def wxPyNonWindowingError(msg
,#output=None,errors=None,
203 stderr
=sys
.__stderr
__,
206 if os
.path
.exists("wxPyNonWindowingErrors.txt"):
210 fl
= open("wxPyNonWindowingErrors.txt",mode
)
211 if stderr
is not None:
212 l
= [fl
,stderr
] # so that the error will be written to the file
213 # before any potential error in stderr.write()... (this is largely
214 # for my own sake in developing...)
218 f
.write(time
.ctime (time
.time ()) + ": ")
221 if sys
.exc_info () [0] is not None:
223 f
.write('Currently handled exception:\n')
225 traceback
.print_exc(file=f
)
227 f
.write('\nPrevious (?) error:\n')
228 elif last
or sys
.last_traceback
:
229 f
.write("\n\n(For wizards only) ")
231 if type(last
) <> types
.ListType
or len(last
) < 3:
232 if (hasattr(sys
,"last_traceback") and sys
.last_traceback
):
233 last
= [sys
.last_type
,sys
.last_value
,sys
.last_traceback
]
234 if type(last
) == types
.ListType
:
235 traceback
.print_exception(last
[0],last
[1],last
[2],
238 if f
is sys
.__stderr
__:
239 s
= ' (see the file "wxPyNonWindowingErrors.txt")'
242 f
.write("Please contact the author with a copy of this\n"
246 if fatal
and stderr
is sys
.__stderr
__:
247 if sys
.platform
in ["windows",'nt',"win32"]:
248 sys
.__stderr
__.write(
249 "\nYou may have to manually close this window to exit.")
252 class wxPythonRExec (rexec
.RExec
):
253 def __init__(self
,securityhole
=0,*args
,**kwargs
):
254 apply(rexec
.RExec
.__init
__, (self
,) + args
, kwargs
)
256 self
.ok_builtin_modules
= self
.ok_builtin_modules
+ \
257 ('wxPython', 'wxPython.wxc','wxPython.wx','wxPython.misc',
258 'wxPython.misc2', 'wxPython.windows', 'wxPython.gdi',
259 'wxPython.clip_dnd', 'wxPython.events', 'wxPython.mdi',
260 'wxPython.frames', 'wxPython.stattool', 'wxPython.controls',
261 'wxPython.controls2', 'wxPython.windows2', 'wxPython.cmndlgs',
262 'wxPython.windows3', 'wxPython.image', 'wxPython.printfw',
263 'wxc','misc', 'misc2', 'windows', 'gdi', 'clip_dnd', 'events',
264 'mdi', 'frames', 'stattool', 'controls', 'controls2', 'windows2',
265 'cmndlgs', 'windows3', 'image', 'printfw', 'wx')
266 # possible security hole!
268 ##def wxPyFatalError(msg,frame=None,**kwargs):
269 ## kwargs.update({'fatal' : 1})
270 ## apply(wxPyNonFatalError,
274 class wxPyNonFatalErrorDialogWithTraceback(wxDialog
):
276 populate_function
= populate_wxNonFatalErrorDialogWithTraceback
277 no_continue_button
= false
280 exitjustreturns
= false
# really only for testing!
282 def __init__(self
, parent
, id,
283 pos
= wxPyDefaultPosition
, size
= wxPyDefaultSize
,
284 style
= wxDEFAULT_DIALOG_STYLE
,
285 programname
= "Python program",
289 disable_exit_button
= false
):
292 sys
.stdout
.write('\nwxPyNonFatalErrorWindow.__init__: '
299 title
= "A (%sfatal) error has occurred in %s!"\
300 % (whetherNF
,programname
)
301 self
.programname
= programname
# save for later use
302 self
.mailto
= mailto
# save for later use
303 self
.parent
= parent
# save for later use
304 self
.whendismissed
= whendismissed
# save for later use
305 self
.dialogtitle
= title
# save for later use
307 wxDialog
.__init
__(self
, parent
, id, title
, pos
, size
, style
)
309 self
.topsizer
= self
.populate_function( false
,true
)
311 self
.SetProgramName(programname
)
312 self
.SetVersion(version
)
314 if not self
.no_continue_button
:
315 EVT_BUTTON(self
, wxPyError_ID_CONTINUE
, self
.OnContinue
)
317 disable_mail_button
= 0
319 disable_mail_button
= 1
320 if not disable_mail_button
:
321 EVT_BUTTON(self
, wxPyError_ID_MAIL
, self
.OnMail
)
323 self
.GetMailButton().Enable(false
)
324 if not disable_exit_button
:
325 EVT_BUTTON(self
, wxPyError_ID_EXIT
, self
.OnExit
)
327 self
.nonwindowingerror
= wxPyNonWindowingErrorHandler(file=sys
.__stderr
__,
331 sys
.stdout
.write('\nwxPyNonFatalErrorWindow.__init__: '
333 def GetExtraInformation(self
):
334 return self
.extraexceptioninformation
336 def SetExtraInformation(self
,value
):
337 self
.extraexceptioninformation
= value
338 c
= self
.GetExtraInformationCtrl()
340 c
.SetLabel(str(value
))
341 self
.topsizer
.Layout()
343 def GetExtraInformationCtrl(self
):
344 return self
.FindWindowById(wxPyError_ID_EXTRAINFORMATION
)
346 def GetExceptionName(self
):
347 return str(self
.exceptiontype
)
349 def SetExceptionName(self
,value
):
350 self
.exceptiontype
= str(value
)
351 c
= self
.GetExceptionNameCtrl()
353 c
.SetLabel(str(value
))
354 self
.topsizer
.Layout()
356 def GetExceptionNameCtrl(self
):
357 return self
.FindWindowById(wxPyError_ID_EXCEPTIONNAME
)
359 def GetTraceback(self
):
361 return self
.traceback
362 except AttributeError:
365 def SetTraceback(self
,value
):
366 self
.traceback
= value
367 c
= self
.GetTracebackCtrl()
369 s
,cs
= c
.GetSize(), c
.GetClientSize()
370 if value
[-1] == '\n':
373 print "%s.SetTraceback(): ...SetValue('%s' (^M=\\r; ^J=\\n))"\
374 % (self
,string
.replace(value
,'\n',"^J"))
377 # Despite using the wxADJUST_MINSIZE flag in the
378 # appropriate AddWindow method of the sizer, this doesn't
379 # size the control appropriately... evidently the control's
380 # GetBestSize method is not returning the "correct"
381 # value... So we perform a rather ugly "fix"... note that
382 # this also requires that we remove the wxADJUST_MINSIZE
383 # flag from the AddWindow method of the sizer containing
384 # the wxTextCtrl, which adds the wxTextCtrl... (this
385 # amounts, as of wxDesigner 2.6, to only a few mouse
389 size
= c
.GetBestSize()
390 print "%s.SetTraceback(): %s.GetBestSize() = (%s,%s)"\
391 % (self
,c
,size
.width
,size
.height
)
393 for v
in string
.split(value
,"\n"):
394 pw
,ph
,d
,e
= t
= c
.GetFullTextExtent(v
)
397 h
= h
+ ph
+ e
# + d# + e
398 pw
= pw
+ wxSystemSettings_GetSystemMetric(wxSYS_VSCROLL_X
)
401 w
= w
+ s
.width
- cs
.width
402 h
= h
+ s
.height
- cs
.height
404 print "%s.SetTraceback(): calculated w,h =" % c
,\
405 w
,h
,"and sys.platform = '%s'" % sys
.platform
406 self
.sizerAroundText
.SetItemMinSize (c
,w
,h
)
408 c
.SetSizeHints (w
,h
,w
,h
)
409 c
.Refresh()#.SetAutoLayout(FALSE)
411 #^ the reason we need the above seems to be to replace the
412 #faulty GetBestSize of wxTextCtrl...
413 #self.sizerAroundText.Layout()
414 self
.topsizer
.Layout()
416 def GetTracebackCtrl(self
):
417 return self
.FindWindowById(wxPyError_ID_TEXTCTRL
)
419 def GetVersion(self
):
422 def SetVersion(self
,value
):
424 c
= self
.GetVersionNumberCtrl()
427 self
.topsizer
.Layout()
429 def GetVersionNumberCtrl(self
):
430 return self
.FindWindowById(wxPyError_ID_VERSIONNUMBER
)
432 def GetProgramName(self
):
433 return self
.programname
435 def SetProgramName(self
,value
):
436 self
.programname
= value
437 c
= self
.GetProgramNameCtrl()
440 self
.topsizer
.Layout()
442 def GetProgramNameCtrl(self
):
443 return self
.FindWindowById(wxPyError_ID_PROGRAMNAME
)
445 def GetContinueButton(self
):
446 return self
.FindWindowById(wxPyError_ID_CONTINUE
)
448 def GetMailButton(self
):
449 return self
.FindWindowById(wxPyError_ID_MAIL
)
451 def GetExitButton(self
):
452 return self
.FindWindowById(wxPyError_ID_EXIT
)
454 # write handler (which is really the guts of the thing...
455 # [Note that this doesn't use sys.excepthook because I already had a
456 # working body of code...
459 if self
.this_exception
is not sys
.last_traceback
:
460 if not wxThread_IsMain():
461 # Aquire the GUI mutex before making GUI calls. Mutex is released
462 # when locker is deleted at the end of this function.
463 locker
= wxMutexGuiLocker()
465 self
.this_exception
= sys
.last_traceback
466 # this is meant to be done once per traceback's sys.stderr.write's
467 # - on the first in fact.....
469 #from wxPython.wx import wxBell
474 'in %s.write(): ' % self
)
476 self
.exceptiontype
= sys
.last_type
477 self
.extraexceptioninformation
= sys
.last_value
478 c
= cStringIO
.StringIO()
479 traceback
.print_last(None,c
)
480 self
.traceback
= c
.getvalue()
484 traceback
.print_last(None,sys
.stdout
)
486 self
.SetExceptionName(str(self
.exceptiontype
))
487 self
.SetExtraInformation(str(self
.extraexceptioninformation
))
488 self
.SetTraceback(str(self
.traceback
))
490 self
.topsizer
.Fit(self
)
491 self
.topsizer
.SetSizeHints(self
)
499 if not locals().has_key("c"):
500 c
= cStringIO
.StringIO()
501 c
.write("[Exception occurred before data from "
502 "sys.last_traceback available]")
503 ## c2 = cStringIO.StringIO()
504 ## traceback.print_exception(None,c2)
505 wxPyNonWindowingError("Warning: "
506 "a %s error was encountered trying to "
507 "handle the exception\n%s\nThis was:"#%s\n"
508 % (sys
.exc_type
, c
.getvalue()),#, c2.getvalue()),
509 stderr
=sys
.__stderr
__,
515 def OnContinue(self
, event
):
517 if self
.whendismissed
:
518 parent
= self
.parent
# so whendismissed can refer to "parent"
521 sys
.stdout
.write("exec '''%s''': "
522 % (self
.whendismissed
))
523 exec self
.whendismissed
524 if _debug
: print "\n",
527 sys
.stdout
.write("wxPythonRExec(%s).r_exec('''%s'''): "
528 % (self
.securityhole
,
530 wxPythonRExec(self
.securityhole
).r_exec(self
.whendismissed
)
531 if _debug
: print "\n",
533 self
.EndModal(wxID_OK
)
536 if _debug
: print "reimporting ",
537 for m
in sys
.modules
.values():
538 if m
and m
.__dict
__["__name__"][0] in string
.uppercase
:#hack!
540 print m
.__dict
__["__name__"],
545 print '\nENDING %s.OnContinue()..\n\n' % (self
,),
547 wxPyNonWindowingError("Warning: the following exception information"
548 " may not be the full story.. (because "
549 "a %s(%s) error was encountered trying to "
550 "handle the exception)\n\n"
551 % tuple(sys
.exc_info()[:2]),
552 stderr
=sys
.__stderr
__,
555 MessageTemplate
= "<head>"\
557 '<body text="#000000" bgcolor="#FFFFFF">'\
559 "<i><b>Hello,</b></i>\n<p>\n"\
560 '<p><h2><font color="#CC6C00">%(programname)s</font>'\
562 "I encountered the following error when running your "\
563 'program <font color="#CC6C00">%(programname)s</font>,'\
564 "at %(date)s.\n<p>\n"\
566 "<h2>Traceback (automatically generated):</h2>\n"\
567 '<p><font size="-1">\n<pre>%(traceback)s</pre>\n<p></font><p>'\
568 "\n<p>\n<h2>More information follows:</h2>\n<p>\n"\
569 '<font color="#CC6C00">'\
571 "information about the error here, such as what you were "\
572 "trying to do at the time of the error. Please "\
573 "understand that failure to fill in this field will be "\
574 "interpreted as an invitation to consign this e-mail "\
575 "straight to the trash can!]\n</i><p>\n"\
577 '<i><b>Yours sincerely,</b></i>\n<p>'\
578 '<font color="#CC6C00">'\
579 "[insert your name here]\n"\
583 def OnMail(self
,event
):
586 print 'Attempting to write mail message.\n',
587 gmtdate
= time
.asctime(time
.gmtime(time
.time())) + ' GMT'
588 tm
= time
.localtime(time
.time())
589 date
= time
.asctime(tm
) + ' ' +\
590 time
.strftime("%Z",tm
)
591 programname
= self
.programname
592 traceback
= self
.traceback
594 message
= self
.MessageTemplate
% vars()
595 subject
= "Un-caught exception when running %s." % programname
597 print 'message:',message
598 print 'subject:,',subject
599 print 'sent to:',mailto
600 mailfrom
= self
.FindWindowById (wxPyError_ID_ADDRESS
)
602 mailfrom
= mailfrom
.GetValue()
603 if _startmailerwithhtml(mailto
,subject
,message
,text
="",mailfrom
=mailfrom
):
604 if not (hasattr(self
,"fatal") and self
.fatal
):
605 self
.OnContinue(event
) # if ok, then act as if "Continue" selected
607 wxPyNonWindowingError("Warning: the following exception information"
608 " may not be the full story... (because "
609 "a %s error was encountered trying to "
610 "handle the original exception)\n\n"#%s"
611 % (sys
.exc_type
,),#self.msg),
612 stderr
=sys
.__stderr
__,
615 def OnExit(self
, event
):
617 self
.EndModal(wxID_CANCEL
)
618 if self
.exitjustreturns
:
620 wxGetApp().ExitMainLoop()
622 ## if isinstance(sys.stderr,wxPyNonFatalErrorDialogWithTraceback):
623 ## if sys.stderr == self:
625 ## sys.stderr.Destroy()
626 ## sys.stderr = wxPyNonWindowingErrorHandler(sys.__stderr__)
627 ## wxSafeYield() # so remaining events are processed...
628 ## if self.parent not in [None,NULL]:
629 ## self.parent.Destroy()
630 ## elif "selfdestroyed" not in locals().keys():
633 def SetText(self
,number
,string
):
634 self
.FindWindowById(eval("wxPyError_ID_TEXT%d"
635 % number
)).SetLabel(string
)
636 self
.topsizer
.Layout()
638 class wxPyFatalErrorDialogWithTraceback(wxPyNonFatalErrorDialogWithTraceback
):
639 populate_function
= populate_wxFatalErrorDialogWithTraceback
640 no_continue_button
= true
643 class wxPyNonFatalErrorDialog(wxPyNonFatalErrorDialogWithTraceback
):
644 populate_function
= populate_wxNonFatalErrorDialog
646 class wxPyFatalErrorDialog(wxPyFatalErrorDialogWithTraceback
):
647 populate_function
= populate_wxFatalErrorDialog
650 def _startmailerwithhtml(mailto
,subject
,html
,text
,mailfrom
=None):
651 if sys
.platform
in ["windows",'nt',"win32"] and sys
.hexversion
>= 0x02000000:
652 name
= tempfile
.mktemp(".eml")
654 f
.write(_createhtmlmail(html
,text
,subject
,to
=mailto
,
660 # probably no association with eml files
661 return _writehtmlmessage(mailto
,subject
,html
,text
,mailfrom
=mailfrom
)
664 return _writehtmlmessage(mailto
,subject
,html
,text
,mailfrom
=mailfrom
)
666 def _writehtmlmessage(mailto
,subject
,html
,text
=None,parent
=None,mailfrom
=None):
667 dlg
= wxFileDialog (parent
,
668 "Please choose a a file to save the message to...",
671 "HTML files (*.htm,*.html)|*.htm,*.html|"
673 wxSAVE | wxHIDE_READONLY
)
674 if dlg
.ShowModal() <> wxID_CANCEL
:
675 f
= open(dlg
.GetPath(),"w")
677 f
.write(_createhtmlmail(html
,text
,subject
,to
=mailto
,mailfrom
=mailfrom
))
683 # PLEASE NOTE THAT THE CODE BELOW FOR STARTING MAILER WITH A GIVEN
684 #(HTML) MESSAGE IS BY ART GILLESPIE [with slight modifications by yours truly].
686 def _createhtmlmail (html
, text
, subject
, to
=None, mailfrom
=None):
687 """Create a mime-message that will render HTML in popular
688 MUAs, text in better ones (if indeed text is not untrue (e.g. None)
690 # imported above #import MimeWriter, mimetools, cStringIO
692 out
= cStringIO
.StringIO() # output buffer for our message
693 htmlin
= cStringIO
.StringIO(html
)
695 txtin
= cStringIO
.StringIO(text
)
697 writer
= MimeWriter
.MimeWriter(out
)
699 # set up some basic headers... we put subject here
700 # because smtplib.sendmail expects it to be in the
704 writer
.addheader("From", mailfrom
)
705 #writer.addheader("Reply-to", mailfrom)
706 writer
.addheader("Subject", subject
)
708 writer
.addheader("To", to
)
709 writer
.addheader("MIME-Version", "1.0")
711 # start the multipart section of the message
712 # multipart/alternative seems to work better
713 # on some MUAs than multipart/mixed
715 writer
.startmultipartbody("alternative")
716 writer
.flushheaders()
718 # the plain text section
721 subpart
= writer
.nextpart()
722 subpart
.addheader("Content-Transfer-Encoding", "quoted-printable")
723 pout
= subpart
.startbody("text/plain", [("charset", 'us-ascii')])
724 mimetools
.encode(txtin
, pout
, 'quoted-printable')
727 # start the html subpart of the message
729 subpart
= writer
.nextpart()
730 subpart
.addheader("Content-Transfer-Encoding", "quoted-printable")
731 pout
= subpart
.startbody("text/html", [("charset", 'us-ascii')])
732 mimetools
.encode(htmlin
, pout
, 'quoted-printable')
735 # Now that we're done, close our writer and
736 # return the message body
743 def _sendmail(mailto
,subject
,html
,text
):# currently unused
744 """For illustration only--this function is not actually used."""
745 message
= _createhtmlmail(html
, text
, subject
)
746 server
= smtplib
.SMTP("localhost")
747 server
.sendmail(mailto
, subject
, message
)