[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[or-cvs] r22014: {weather} - Code cleanups - Make user input validation more bulletproo (in weather/trunk: . lib/weather)
Author: kaner
Date: 2010-03-18 17:02:56 +0000 (Thu, 18 Mar 2010)
New Revision: 22014
Modified:
weather/trunk/Weather.py
weather/trunk/lib/weather/config.py
weather/trunk/lib/weather/constants.py
weather/trunk/lib/weather/poller.py
weather/trunk/lib/weather/queries.py
weather/trunk/lib/weather/torping.py
weather/trunk/lib/weather/utils.py
Log:
- Code cleanups
- Make user input validation more bulletproof
- Introduce logging
Modified: weather/trunk/Weather.py
===================================================================
--- weather/trunk/Weather.py 2010-03-18 09:06:10 UTC (rev 22013)
+++ weather/trunk/Weather.py 2010-03-18 17:02:56 UTC (rev 22014)
@@ -9,48 +9,85 @@
import os
import re
import sys
+import logging
from twisted.web import resource, static, server, http
from twisted.enterprise import adbapi
from twisted.internet import reactor
from twisted.internet.task import LoopingCall
from weather.torping import TorPing
-from weather.constants import PAGE_TEMPLATE, PAGE_SIGNUP, CONFIRMATION_MAIL, THANKS_OUT, PAGE_SUB_FIN, SUBS_MAIL
-from weather.queries import CHECK_SUBS_Q, INSERT_SUBS_Q, CHECK_SUBS_AUTH_Q, ACK_SUB_Q, UNSUBSCRIBE_Q
+import weather.constants as constants
+import weather.queries as queries
from weather.poller import WeatherPoller
-from weather.config import pollPeriod, URLbase, mailFrom, databaseName
+import weather.config as config
import weather.utils as utils
class WeatherIndex(resource.Resource):
+ """The index page"""
def render(self, request):
- return utils.pageOut(PAGE_SIGNUP)
+ return utils.pageOut(constants.PAGE_SIGNUP)
class SubscribeRequest(resource.Resource):
+ """Implementation of the 'page' that receives a subscription request by
+ a user."""
def __init__(self, dbConn):
self.dbConn = dbConn
resource.Resource.__init__(self)
- def render(self, request):
- self.request = request
- self.email = request.args['email'][0]
- self.node = request.args['node'][0]
- self.downtime = int(request.args['downtime'][0])
+ def _parseEmail(self):
+ if not self.request.args.has_key("email"):
+ logging.error("Request provided no email address")
+ return False
+ self.email = self.request.args['email'][0]
+ if not utils.checkMail(self.email):
+ logging.error("Bad email address '%s'" % self.email)
+ return False
+ return True
+
+ def _parseNodeID(self):
+ if not self.request.args.has_key("node"):
+ logging.error("Request provided no node ID")
+ return False
+ self.node = self.request.args['node'][0]
+ if not utils.checkNodeID(self.node):
+ logging.error("Bad node ID '%s'" % self.email)
+ return False
+ return True
+
+ def _parseDowntime(self):
+ if not self.request.args.has_key("downtime"):
+ logging.error("Request provided no downtime number")
+ return False
+ self.downtime = 1
+ try:
+ self.downtime = int(self.request.args['downtime'][0])
+ except:
+ d = self.request.args['downtime'][0]
+ logging.error("Bad downtime number: '%s'" % d)
+ return False
# Don't accept downtime grace values longer than 8760 hours = 1 year
if self.downtime > 8760:
self.downtime = 8760
if self.downtime < 1:
self.downtime = 1
+ return True
- if not utils.checkMail(self.email):
- return "Error: Bad email address '%s'" % self.email
+ def render(self, request):
+ self.request = request
+ if not self._parseEmail():
+ return self._errOut("Please check the email address you entered")
+ if not self._parseNodeID():
+ return self._errOut("Please check the node ID you entered")
+ if not self._parseDowntime():
+ return self._errOut("Please check the downtime hours you entered")
self.subs_auth = utils.getRandString()
self.unsubs_auth = utils.getRandString()
- self._isSubscribedAlready()
+ self._checkSubscription()
return server.NOT_DONE_YET
- def _isSubscribedAlready(self):
- dbQuery = CHECK_SUBS_Q % (self.email, self.node)
+ def _checkSubscription(self):
+ dbQuery = queries.CHECK_SUBS_Q % (self.email, self.node)
self.dbConn.runQuery(dbQuery).addCallback(
self._checkHasSubRet).addErrback(
self._errback)
@@ -58,135 +95,153 @@
def _checkHasSubRet(self, result):
# Do we already have a subscription for this address for this node?
if len(result) is not 0:
- self.request.setResponseCode(http.OK)
- self.request.write(utils.pageOut("Error: Already subscribed."))
- self.request.finish()
+ self._errOut("Error: Already subscribed.")
else:
# Alright, subscribe it
self._runSaveQuery()
def _runSaveQuery(self):
- dbQuery = INSERT_SUBS_Q % (self.email, self.node, \
+ dbQuery = queries.INSERT_SUBS_Q % (self.email, self.node, \
self.subs_auth, self.unsubs_auth, self.downtime)
self.dbConn.runOperation(dbQuery).addCallback(
self._saved).addErrback(
self._errback)
def _saved(self, result):
- url = URLbase + "/confirm-subscribe?auth=" + self.subs_auth
+ url = config.URLbase + "/confirm-subscribe?auth=" + self.subs_auth
try:
- mailText = CONFIRMATION_MAIL % (self.node, url)
+ mailText = constants.CONFIRMATION_MAIL % (self.node, url)
subject = "Confirmation needed"
- utils.sendMail(mailFrom, self.email, mailText, subject)
+ utils.sendMail(config.mailFrom, self.email, mailText, subject)
except Exception, e:
- self.error = "Unknown error while sending confirmation mail. " + \
- "Please try again later."
- # "[Exception %s]" % sys.exc_info()[0]
- self._rollBack()
+ error = "Unknown error while sending confirmation mail. " + \
+ "Please try again later."
+ logging.error(e)
+ self._rollBack(error)
return
-
self.request.setResponseCode(http.OK)
- text = THANKS_OUT % self.email
+ text = constants.THANKS_OUT % self.email
self.request.write(utils.pageOut(text))
self.request.finish()
- def _rollBack(self):
- dbQuery = CHECK_SUBS_Q % (self.email, self.node)
+ def _rollBack(self, errorMsg):
+ dbQuery = queries.ROLLBACK_Q % (self.email, self.node)
self.dbConn.runQuery(dbQuery).addCallback(
- self._errOut).addErrback(
+ self._errOut, errorMsg).addErrback(
self._errback)
- def _errOut(self):
+ def _errback(self, failure):
+ self._errOut("Error in subscribe: %s" % (failure.getErrorMessage()))
+
+ def _errOut(self, error):
+ logging.error(error)
self.request.setResponseCode(http.INTERNAL_SERVER_ERROR)
- self.request.write(utils.pageOut(self.error))
+ self.request.write(utils.pageOut(error))
self.request.finish()
+ return server.NOT_DONE_YET
- def _errback(self, failure):
- self.error = "Error: %s" % (failure.getErrorMessage())
- self._errOut()
class ConfirmSubscribeRequest(resource.Resource):
+ """The implementation of the 'page' the user gets to see if he confirms
+ his subscription"""
def __init__(self, dbConn):
self.dbConn = dbConn
resource.Resource.__init__(self)
def render(self, request):
- self.subs_auth = request.args['auth'][0]
- self._lookupSubsAuth(request)
+ self.request = request
+ if not self.request.args.has_key("auth"):
+ self._errOut("Bad auth string")
+ return server.NOT_DONE_YET
+ self.subs_auth = self.request.args['auth'][0]
+ self._lookupSubsAuth()
return server.NOT_DONE_YET
- def _lookupSubsAuth(self, request):
- dbQuery = CHECK_SUBS_AUTH_Q % self.subs_auth
+ def _lookupSubsAuth(self):
+ dbQuery = queries.CHECK_SUBS_AUTH_Q % self.subs_auth
self.dbConn.runQuery(dbQuery).addCallback(
- self._checkRet, request).addErrback(
- self._errback, request)
+ self._checkRet).addErrback(
+ self._sqlErrback)
- def _checkRet(self, result, request):
+ def _checkRet(self, result):
if len(result) is 0:
- request.setResponseCode(http.OK)
- text = "Error: No subscription with your auth code"
- request.write(utils.pageOut(text))
- request.finish()
+ error = "Error: No unconfirmed subscription with given auth code"
+ self._errOut(error)
else:
self.unsubs_auth = str(result[0][0])
self.node = str(result[0][1])
self.email = str(result[0][2])
- return self._ackSubscription(request)
+ return self._ackSubscription()
- def _ackSubscription(self, request):
- dbQuery = ACK_SUB_Q % self.subs_auth
+ def _ackSubscription(self):
+ dbQuery = queries.ACK_SUB_Q % self.subs_auth
self.dbConn.runQuery(dbQuery).addCallback(
- self._subDone, request).addErrback(
- self._errback, request)
+ self._subDone).addErrback(
+ self._sqlErrback)
- def _subDone(self, result, request):
- url = URLbase + "/unsubscribe?auth=" + self.unsubs_auth
+ def _subDone(self, result):
+ url = config.URLbase + "/unsubscribe?auth=" + self.unsubs_auth
link = "<a href=\"" + url + "\">" + url + "</a>"
- pageout = PAGE_SUB_FIN % link
+ pageout = constants.PAGE_SUB_FIN % link
try:
- mailText = SUBS_MAIL % (self.node, url)
+ mailText = constants.SUBS_MAIL % (self.node, url)
subject = "Subscription successfull"
- utils.sendMail(mailFrom, self.email, mailText, subject)
+ utils.sendMail(config.mailFrom, self.email, mailText, subject)
except Exception, e:
pageout += "\n\nUnknown error while sending confirmation mail."
- print e
- request.write(utils.pageOut(pageout))
- request.finish()
+ logging.error("Error while confirmation mail: %s" % e)
+ self.request.write(utils.pageOut(pageout))
+ self.request.finish()
- def _errback(self, failure, request):
- request.setResponseCode(http.INTERNAL_SERVER_ERROR)
- text = "Error: %s" % (failure.getErrorMessage())
- request.write(utils.pageOut(text))
- request.finish()
+ def _sqlErrback(self, failure):
+ error = "Error in ConfirmSubscribe: %s" % (failure.getErrorMessage())
+ self._errOut(error)
+ def _errOut(self, error):
+ logging.error(error)
+ self.request.setResponseCode(http.INTERNAL_SERVER_ERROR)
+ self.request.write(utils.pageOut(error))
+ self.request.finish()
+
class UnsubscribeRequest(resource.Resource):
+ """The implementation of the 'page' the user gets to see if he wishes to
+ unsubscribe"""
def __init__(self, dbConn):
self.dbConn = dbConn
resource.Resource.__init__(self)
def render(self, request):
+ self.request = request
+ if not self.request.args.has_key("auth"):
+ self._errOut("Bad auth value")
+ return server.NOT_DONE_YET
self.unsubs_auth = request.args['auth'][0]
- self._deleteSub(request)
+ self._deleteSub()
return server.NOT_DONE_YET
- def _deleteSub(self, request):
- dbQuery = UNSUBSCRIBE_Q % self.unsubs_auth
+ def _deleteSub(self):
+ dbQuery = queries.UNSUBSCRIBE_Q % self.unsubs_auth
self.dbConn.runQuery(dbQuery).addCallback(
- self._deleteDone, request).addErrback(
- self._errback, request)
+ self._deleteDone).addErrback(
+ self._errback)
- def _deleteDone(self, result, request):
- request.setResponseCode(http.OK)
- request.write(utils.pageOut("Subscription deleted. Goodbye."))
- request.finish()
+ def _deleteDone(self, result):
+ self.request.setResponseCode(http.OK)
+ self.request.write(utils.pageOut("Subscription deleted. Goodbye."))
+ self.request.finish()
- def _errback(self, failure, request):
- request.setResponseCode(http.INTERNAL_SERVER_ERROR)
- text = "Error: %s" % (failure.getErrorMessage())
- request.write(utils.pageOut(text))
- request.finish()
+ def _errback(self, failure):
+ error = "Error in UnsubscribeRequest: %s" % (failure.getErrorMessage())
+ self._errOut(error)
+ def _errOut(self, error):
+ logging.error(error)
+ self.request.setResponseCode(http.INTERNAL_SERVER_ERROR)
+ self.request.write(utils.pageOut(error))
+ self.request.finish()
+
class RootResource(resource.Resource):
+ """Root resource: Collect all resources needed by weather"""
def __init__(self, dbConn):
resource.Resource.__init__(self)
self.putChild('top-left.png', static.File("./top-left.png"))
@@ -199,17 +254,20 @@
self.putChild('unsubscribe', UnsubscribeRequest(dbConn))
def main():
+ # Set up logging
+ utils.initLogging()
# Set up database connection
- dbConn = utils.setupDBConn(databaseName)
+ dbConn = utils.setupDBConn(config.databaseName)
# Set up polling timer
# XXX Have one main TorPing instance until TotCtl fixes its thread leak
torPing = TorPing()
weatherPoller = WeatherPoller(dbConn, torPing)
pollTimer = LoopingCall(weatherPoller.poller)
- pollTimer.start(pollPeriod)
+ pollTimer.start(config.pollPeriod)
# Set up webserver
weatherSite = server.Site(RootResource(dbConn))
reactor.listenTCP(8000, weatherSite)
+ logging.info("Weather service started")
reactor.run()
if __name__ == "__main__":
Modified: weather/trunk/lib/weather/config.py
===================================================================
--- weather/trunk/lib/weather/config.py 2010-03-18 09:06:10 UTC (rev 22013)
+++ weather/trunk/lib/weather/config.py 2010-03-18 17:02:56 UTC (rev 22014)
@@ -1,18 +1,15 @@
-#!/usr/bin/env python2.5
+# Tor Weather
+# by Jacob Appelbaum <jacob@xxxxxxxxxxxxx>, Christian Fromme <kaner@xxxxxxxxxx>
+# Copyright (c) 2009, 2010 The Tor Project
+# See LICENSE for licensing information
URLbase = "https://weather.torproject.org"
+# XXX: Make bulletproof
authenticator = open("auth_token").read().strip()
mailFrom = "tor-ops@xxxxxxxxxxxxxx"
-# these respond to pings (for now!) and are geographically dispersed
-
-pingTargets = ["google.com", "telstra.com.au", "yahoo.co.uk"]
-
-failureThreshold = 4 # this number of failures in a row counts as being
- # down
-
pollPeriod = 3600 # Check every hour
databaseName = "subscriptions.db"
Modified: weather/trunk/lib/weather/constants.py
===================================================================
--- weather/trunk/lib/weather/constants.py 2010-03-18 09:06:10 UTC (rev 22013)
+++ weather/trunk/lib/weather/constants.py 2010-03-18 17:02:56 UTC (rev 22014)
@@ -1,5 +1,7 @@
-#!/usr/bin/python
-# Some constants needed in weather
+# Tor Weather
+# by Jacob Appelbaum <jacob@xxxxxxxxxxxxx>, Christian Fromme <kaner@xxxxxxxxxx>
+# Copyright (c) 2009, 2010 The Tor Project
+# See LICENSE for licensing information
THANKS_OUT = """
Thanks for using tor weather. A confirmation request has been sent to '%s'.
@@ -28,7 +30,7 @@
Somebody (possibly you) has requested that status monitoring information
about a tor node (id: %s) be sent to this email address.
- If you wish to confirm this request, please visit the following link:
+ If you wish to confirm this request, please visit the following url:
%s
@@ -100,7 +102,7 @@
</p>
<p>
How many hours of downtime until we send a notification:<br>
-<input type="text" name="downtime" size="50" maxlength="255" value="Default is 1 hour, enter up to 8760 (1 year)" onclick="if (this.value == 'Default is 1 hour, enter up to 8760 (1 year)') {this.value = '1'}" />
+<input type="text" name="downtime" size="50" maxlength="255" value="Default is 1 hour, enter up to 8760 (1 year)" onclick="if (this.value == 'Default is 1 hour, enter up to 8760 (1 year)') {this.value = 'Default is 1 hour, enter up to 8760 (1 year)'}" />
</p>
<p>
<input type="submit" class="submit" value="Subscribe to Tor Weather" name="sa"/>
Modified: weather/trunk/lib/weather/poller.py
===================================================================
--- weather/trunk/lib/weather/poller.py 2010-03-18 09:06:10 UTC (rev 22013)
+++ weather/trunk/lib/weather/poller.py 2010-03-18 17:02:56 UTC (rev 22014)
@@ -1,12 +1,16 @@
-#!/usr/bin/python
+# Tor Weather
+# by Jacob Appelbaum <jacob@xxxxxxxxxxxxx>, Christian Fromme <kaner@xxxxxxxxxx>
+# Copyright (c) 2009, 2010 The Tor Project
+# See LICENSE for licensing information
+import logging
import smtplib
+from twisted.web import server
+from weather.torping import TorPing
+import weather.config as config
+import weather.constants as constants
import weather.queries as queries
-from twisted.web import server
-from weather.torping import TorPing
-from weather.config import URLbase, mailFrom
-from weather.constants import REPORT_MAIL
import weather.utils as utils
class WeatherPoller():
@@ -29,16 +33,15 @@
for result in resultList:
checkHost = result[2]
if not self._checkHost(checkHost):
- print "Server %s seems to be offline" % checkHost
+ logging.info("Server %s seems to be offline" % checkHost)
self._handleOfflineNode(result)
else:
- print "Server %s is ok" % checkHost
+ logging.info("Server %s is ok" % checkHost)
# Reset possible seen_down counter
self._resetSeendown(result)
def _errBack(self, failure):
- # XXX: Log
- print "Error: ", failure.getErrorMessage()
+ logging.error("Error: %s" % failure.getErrorMessage())
def _checkHost(self, hostID):
return self.torPing.ping(hostID)
@@ -62,18 +65,17 @@
noticed = result[0][8]
unsubs_auth = result[0][4]
if noticed is 0 and (seenDown + 1) >= downGrace:
- # XXX: Log
+ logging.info("Sending report to %s about %s" % (email, node))
self._sendNotice(email, node, id, unsubs_auth)
def _sendNotice(self, email, node, id, unsubs_auth):
- unsubsURL = URLbase + "/unsubscribe?auth=" + str(unsubs_auth)
- mailText = REPORT_MAIL % (node, unsubsURL)
+ unsubsURL = config.URLbase + "/unsubscribe?auth=" + str(unsubs_auth)
+ mailText = constants.REPORT_MAIL % (node, unsubsURL)
try:
subject = "Report"
- utils.sendMail(mailFrom, email, mailText, subject)
+ utils.sendMail(config.mailFrom, email, mailText, subject)
except:
- # XXX: Log
- print "Error, exception!"
+ logging.error("Could not send weather report for %s", email)
else:
# Set 'noticed' flag so we don't bother that user again
self._setNoticed(id)
@@ -91,7 +93,7 @@
self._errBack)
def _updateDone(self, result):
- print "Query ok"
+ pass
def _resetSeendown(self, dbRow):
dbQuery = queries.RESET_COUNT_Q % dbRow[0]
Modified: weather/trunk/lib/weather/queries.py
===================================================================
--- weather/trunk/lib/weather/queries.py 2010-03-18 09:06:10 UTC (rev 22013)
+++ weather/trunk/lib/weather/queries.py 2010-03-18 17:02:56 UTC (rev 22014)
@@ -1,7 +1,11 @@
-#!/usr/bin/python
+# Tor Weather
+# by Jacob Appelbaum <jacob@xxxxxxxxxxxxx>, Christian Fromme <kaner@xxxxxxxxxx>
+# Copyright (c) 2009, 2010 The Tor Project
+# See LICENSE for licensing information
# Query strings
CHECK_SUBS_Q = "SELECT id FROM subscriptions WHERE email='%s' and node='%s'"
+ROLLBACK_Q = "DELETE FROM subscriptions WHERE email='%s' and node='%s'"
INSERT_SUBS_Q = """
INSERT INTO subscriptions (email, node, subs_auth, unsubs_auth, subscribed, downtime_grace, seen_down, noticed) VALUES ('%s', '%s', '%s', '%s', 0, '%s', 0, 0)
"""
Modified: weather/trunk/lib/weather/torping.py
===================================================================
--- weather/trunk/lib/weather/torping.py 2010-03-18 09:06:10 UTC (rev 22013)
+++ weather/trunk/lib/weather/torping.py 2010-03-18 17:02:56 UTC (rev 22014)
@@ -1,9 +1,14 @@
-#!/usr/bin/python
+# Tor Weather
+# by Jacob Appelbaum <jacob@xxxxxxxxxxxxx>, Christian Fromme <kaner@xxxxxxxxxx>
+# Copyright (c) 2009, 2010 The Tor Project
+# See LICENSE for licensing information
# Taken from the old weather code
+import sys
+import logging
import socket
from TorCtl import TorCtl
-from weather.config import authenticator
+import weather.config
debugfile = open("debug", "w")
@@ -14,9 +19,17 @@
self.control_host = control_host
self.control_port = control_port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.sock.connect((control_host,control_port))
+ try:
+ self.sock.connect((control_host,control_port))
+ except:
+ errormsg = "Could not connect to Tor control port" + \
+ "Is Tor running on %s with its control port opened on %s?" \
+ % (control_host, control_port)
+ logging.error(errormsg)
+ print >> sys.stderr, errormsg
+ raise
self.control = TorCtl.Connection(self.sock)
- self.control.authenticate(authenticator)
+ self.control.authenticate(weather.config.authenticator)
self.control.debug(debugfile)
def __del__(self):
@@ -28,6 +41,7 @@
try:
self.control.close()
except:
+ logging.error("Exception while closing TorCtl")
pass
del self.control
@@ -37,15 +51,15 @@
"Let's see if this tor node is up by only asking Tor."
try:
info = self.control.get_info(str("ns/id/" + nodeId))
- except TorCtl.ErrorReply:
+ except TorCtl.ErrorReply, e:
# If we're getting here, we're likely seeing:
# ErrorReply: 552 Unrecognized key "ns/id/46D9..."
# This means that the node isn't recognized by
- # XXX: Log
+ logging.error("ErrorReply: %s" % str(e))
return False
except:
- # XXX: Log
+ logging.error("Unknown exception in ping()")
return False
# If we're here, we were able to fetch information about the router
Modified: weather/trunk/lib/weather/utils.py
===================================================================
--- weather/trunk/lib/weather/utils.py 2010-03-18 09:06:10 UTC (rev 22013)
+++ weather/trunk/lib/weather/utils.py 2010-03-18 17:02:56 UTC (rev 22014)
@@ -1,4 +1,7 @@
-#!/usr/bin/python
+# Tor Weather
+# by Jacob Appelbaum <jacob@xxxxxxxxxxxxx>, Christian Fromme <kaner@xxxxxxxxxx>
+# Copyright (c) 2009, 2010 The Tor Project
+# See LICENSE for licensing information
import os
import re
@@ -6,19 +9,20 @@
import base64
import sqlite3
import smtplib
+import logging
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from twisted.enterprise import adbapi
-from weather.constants import PAGE_TEMPLATE, PAGE_SIGNUP
-from weather.queries import CREATE_TABLE_Q
+import weather.constants as constants
+import weather.queries as queries
def setupDBConn(databaseName):
"""Create database and table in case they don't exist yet, return pool"""
db = sqlite3.connect(databaseName)
- db.execute(CREATE_TABLE_Q % "subscriptions")
+ db.execute(queries.CREATE_TABLE_Q % "subscriptions")
db.close()
dbConn = adbapi.ConnectionPool("sqlite3", databaseName, check_same_thread=False)
return dbConn
@@ -55,10 +59,10 @@
r = base64.urlsafe_b64encode(os.urandom(18))[:-1]
# some email clients don't like URLs ending in -
if r.endswith("-"):
- r.replace("-", "x")
+ r = r.replace("-", "x")
return r
-def isValidNodeID(nodeID):
+def checkNodeID(nodeID):
"""Check if a given Tor node ID looks ok"""
nodeOk = re.compile("(0x)?[a-fA-F0-9]{40}\Z")
if nodeOk.match(nodeID):
@@ -68,7 +72,7 @@
def pageOut(text):
"""Our great template engine ;-)"""
- return PAGE_TEMPLATE % text
+ return constants.PAGE_TEMPLATE % text
def sendMail(fromPart, toPart, messageText, subject=""):
"""Send a certain mail text with certain From: and certain To: field"""
@@ -86,3 +90,9 @@
smtp = smtplib.SMTP("localhost:25")
smtp.sendmail(fromPart, toPart, message.as_string())
smtp.quit()
+
+def initLogging(loglevel=logging.INFO,
+ logformat='%(asctime)-15s (%(process)d) %(message)s',
+ logfile='./weather.log'):
+ """Very basic log setup"""
+ logging.basicConfig(format=logformat, level=loglevel, filename=logfile)