1 """
2 Implements the Lamson command line tool's commands, which are run
3 by the lamson.args module dynamically. Each command has it's
4 actual user displayed command line documentation as the __doc__
5 string.
6
7 You will notice that all of the command functions in this module
8 end in _command. This is not required by the lamson.args module
9 but it is the default. You could easily use any other suffix, or
10 none at all.
11
12 This is done to disambiguate the command that it implements
13 so that your command line tools do not clash with Python's
14 reserved words and built-ins. With this design you can have a
15 list_command without clashing with list().
16
17 You will also notice that commands which take trailing positional
18 arguments give a TRAILING=[] or TRAILING=None (if it's required).
19 This is done instead of *args because we need to use None to indicate
20 that this command requires positional arguments. TRAILING=[] is
21 like saying they are optional (but expected), and TRAILING=None is
22 like saying they are required. You can't (afaik) do TRAILING=None
23 with *args.
24
25 See lamson.args for more details.
26 """
27
28 from lamson import server, args, utils, mail, routing, queue, encoding
29 from pkg_resources import resource_stream
30 from zipfile import ZipFile
31 import glob
32 import lamson
33 import os
34 import signal
35 import sys
36 import time
37 import mailbox
38 import email
39
40 -def log_command(port=8825, host='127.0.0.1', chroot=False,
41 chdir=".", uid=False, gid=False, umask=False, pid="./run/log.pid",
42 FORCE=False):
43 """
44 Runs a logging only server on the given hosts and port. It logs
45 each message it receives and also stores it to the run/queue
46 so that you can make sure it was received in testing.
47
48 lamson log -port 8825 -host 127.0.0.1 \\
49 -pid ./run/log.pid -chroot False \\
50 -chdir "." -umask False -uid False -gid False \\
51 -FORCE False
52
53 If you specify a uid/gid then this means you want to first change to
54 root, set everything up, and then drop to that UID/GID combination.
55 This is typically so you can bind to port 25 and then become "safe"
56 to continue operating as a non-root user.
57
58 If you give one or the other, this it will just change to that
59 uid or gid without doing the priv drop operation.
60 """
61 loader = lambda: utils.make_fake_settings(host, port)
62 utils.start_server(pid, FORCE, chroot, chdir, uid, gid, umask, loader)
63
64
65 -def send_command(port=8825, host='127.0.0.1', username=False, password=False,
66 ssl=False, starttls=False, debug=1, sender=None, to=None,
67 subject=None, body=None, attach=False):
68 """
69 Sends an email to someone as a test message.
70 See the sendmail command for a sendmail replacement.
71
72 lamson send -port 8825 -host 127.0.0.1 -debug 1 \\
73 -sender EMAIL -to EMAIL -subject STR -body STR -attach False'
74
75 There is also a username, password, and starttls option for those
76 who need it.
77 """
78 message = mail.MailResponse(From=sender,
79 To=to,
80 Subject=subject,
81 Body=body)
82 if attach:
83 message.attach(attach)
84
85 if username == False:
86 username = None
87 if password == False:
88 password = None
89
90 relay = server.Relay(host, port=port, username=username, password=password,
91 ssl=ssl, starttls=starttls, debug=debug)
92 relay.deliver(message)
93
94
96 """
97 Used as a testing sendmail replacement for use in programs
98 like mutt as an MTA. It reads the email to send on the stdin
99 and then delivers it based on the port and host settings.
100
101 lamson sendmail -port 8825 -host 127.0.0.1 -debug 0 -- [recipients]
102 """
103 relay = server.Relay(host, port=port,
104 debug=debug)
105 data = sys.stdin.read()
106 msg = mail.MailRequest(None, TRAILING, None, data)
107 relay.deliver(msg)
108
109
110
111
112 -def start_command(pid='./run/smtp.pid', FORCE=False, chroot=False, chdir=".",
113 boot="config.boot", uid=False, gid=False, umask=False):
114 """
115 Runs a lamson server out of the current directory:
116
117 lamson start -pid ./run/smtp.pid -FORCE False -chroot False -chdir "." \\
118 -umask False -uid False -gid False -boot config.boot
119 """
120 loader = lambda: utils.import_settings(True, from_dir=os.getcwd(), boot_module=boot)
121 utils.start_server(pid, FORCE, chroot, chdir, uid, gid, umask, loader)
122
123
124 -def stop_command(pid='./run/smtp.pid', KILL=False, ALL=False):
125 """
126 Stops a running lamson server. Give -KILL True to have it
127 stopped violently. The PID file is removed after the
128 signal is sent. Give -ALL the name of a run directory and
129 it will stop all pid files it finds there.
130
131 lamson stop -pid ./run/smtp.pid -KILL False -ALL False
132 """
133 pid_files = []
134
135 if ALL:
136 pid_files = glob.glob(ALL + "/*.pid")
137 else:
138 pid_files = [pid]
139
140 if not os.path.exists(pid):
141 print "PID file %s doesn't exist, maybe Lamson isn't running?" % pid
142 sys.exit(1)
143 return
144
145 print "Stopping processes with the following PID files: %s" % pid_files
146
147 for pid_f in pid_files:
148 pid = open(pid_f).readline()
149
150 print "Attempting to stop lamson at pid %d" % int(pid)
151
152 try:
153 if KILL:
154 os.kill(int(pid), signal.SIGKILL)
155 else:
156 os.kill(int(pid), signal.SIGHUP)
157
158 os.unlink(pid_f)
159 os.unlink(pid_f + ".lock")
160 except OSError, exc:
161 print "ERROR stopping Lamson on PID %d: %s" % (int(pid), exc)
162
163
165 """
166 Simply attempts a stop and then a start command. All options for both
167 apply to restart. See stop and start for options available.
168 """
169
170 stop_command(**options)
171 time.sleep(2)
172 start_command(**options)
173
174
176 """
177 Prints out status information about lamson useful for finding out if it's
178 running and where.
179
180 lamson status -pid ./run/smtp.pid
181 """
182 if os.path.exists(pid):
183 pid = open(pid).readline()
184 print "Lamson running with PID %d" % int(pid)
185 else:
186 print "Lamson not running."
187
188
209
210
211 -def queue_command(pop=False, get=False, keys=False, remove=False, count=False,
212 clear=False, name="run/queue"):
213 """
214 Let's you do most of the operations available to a queue.
215
216 lamson queue (-pop | -get | -remove | -count | -clear | -keys) -name run/queue
217 """
218 print "Using queue: %r" % name
219
220 inq = queue.Queue(name)
221
222 if pop:
223 key, msg = inq.pop()
224 if key:
225 print "KEY: ", key
226 print msg
227 elif get:
228 print inq.get(get)
229 elif remove:
230 inq.remove(remove)
231 elif count:
232 print "Queue %s contains %d messages" % (name, inq.count())
233 elif clear:
234 inq.clear()
235 elif keys:
236 print "\n".join(inq.keys())
237 else:
238 print "Give something to do. Try lamson help -for queue to find out what."
239 sys.exit(1)
240 return
241
242
243 -def routes_command(TRAILING=['config.testing'], path=os.getcwd(), test=""):
244 """
245 Prints out valuable information about an application's routing configuration
246 after everything is loaded and ready to go. Helps debug problems with
247 messages not getting to your handlers. Path has the search paths you want
248 separated by a ':' character, and it's added to the sys.path.
249
250 lamson routes -path $PWD -- config.testing -test ""
251
252 It defaults to running your config.testing to load the routes.
253 If you want it to run the config.boot then give that instead:
254
255 lamson routes -- config.boot
256
257 You can also test a potential target by doing -test EMAIL.
258
259 """
260 modules = TRAILING
261 sys.path += path.split(':')
262 test_case_matches = []
263
264 for module in modules:
265 __import__(module, globals(), locals())
266
267 print "Routing ORDER: ", routing.Router.ORDER
268 print "Routing TABLE: \n---"
269 for format in routing.Router.REGISTERED:
270 print "%r: " % format,
271 regex, functions = routing.Router.REGISTERED[format]
272 for func in functions:
273 print "%s.%s " % (func.__module__, func.__name__),
274 match = regex.match(test)
275 if test and match:
276 test_case_matches.append((format, func, match))
277
278 print "\n---"
279
280 if test_case_matches:
281 print "\nTEST address %r matches:" % test
282 for format, func, match in test_case_matches:
283 print " %r %s.%s" % (format, func.__module__, func.__name__)
284 print " - %r" % (match.groupdict())
285 elif test:
286 print "\nTEST address %r didn't match anything." % test
287
288
289
291 """
292 Generates various useful things for you to get you started.
293
294 lamson gen -project STR -FORCE False
295 """
296 project = project
297
298 if os.path.exists(project) and not FORCE:
299 print "Project %s exists, delete it first." % project
300 sys.exit(1)
301 return
302
303 prototype = ZipFile(resource_stream(__name__, 'data/prototype.zip'))
304
305
306 if not os.path.exists(project):
307 os.makedirs(project)
308
309 files = prototype.namelist()
310
311 for gen_f in files:
312 if str(gen_f).endswith('/'):
313 target = os.path.join(project, gen_f)
314 if not os.path.exists(target):
315 print "mkdir: %s" % target
316 os.makedirs(target)
317 else:
318 target = os.path.join(project, gen_f)
319 if os.path.exists(target):
320 continue
321
322 print "copy: %s" % target
323 out = open(target, 'w')
324 out.write(prototype.read(gen_f))
325 out.close()
326
327
328 -def web_command(basedir=".", port=8888, host='127.0.0.1'):
329 """
330 Starts a very simple files only web server for easy testing of applications
331 that need to make some HTML files as the result of their operation.
332 If you need more than this then use a real web server.
333
334 lamson web -basedir "." -port 8888 -host '127.0.0.1'
335
336 This command doesn't exit so you can view the logs it prints out.
337 """
338 from BaseHTTPServer import HTTPServer
339 from SimpleHTTPServer import SimpleHTTPRequestHandler
340
341 os.chdir(basedir)
342 web = HTTPServer((host, port), SimpleHTTPRequestHandler)
343 print "Starting server on %s:%d out of directory %r" % (
344 host, port, basedir)
345 web.serve_forever()
346
347
349 """
350 Uses Lamson mail cleansing and canonicalization system to take an
351 input maildir (or mbox) and replicate the email over into another
352 maildir. It's used mostly for testing and cleaning.
353 """
354 error_count = 0
355
356 try:
357 inbox = mailbox.mbox(input)
358 except:
359 inbox = mailbox.Maildir(input, factory=None)
360
361 outbox = mailbox.Maildir(output)
362
363 for msg in inbox:
364 try:
365 mail = encoding.from_message(msg)
366 outbox.add(encoding.to_string(mail))
367 except encoding.EncodingError, exc:
368 print "ERROR: ", exc
369 error_count += 1
370
371 outbox.close()
372 inbox.close()
373
374 print "TOTAL ERRORS:", error_count
375
376
377 -def blast_command(input=None, host='127.0.0.1', port=8823, debug=0):
378 """
379 Given a maildir, this command will go through each email
380 and blast it at your server. It does nothing to the message, so
381 it will be real messages hitting your server, not cleansed ones.
382 """
383 inbox = mailbox.Maildir(input)
384 relay = server.Relay(host, port=port, debug=debug)
385
386 for key in inbox.keys():
387 msgfile = inbox.get_file(key)
388 msg = email.message_from_file(msgfile)
389 relay.deliver(msg)
390
391
393 """
394 Prints the version of Lamson, the reporitory revision, and the
395 file it came from.
396 """
397
398 from lamson import version
399
400 print "Lamson-Version: ", version.VERSION['version']
401 print "Repository-Revision:", version.VERSION['rev'][0]
402 print "Repository-Hash:", version.VERSION['rev'][1]
403 print "Version-File:", version.__file__
404 print ""
405 print "Lamson is Copyright (C) Zed A. Shaw 2008-2009. Licensed GPLv3."
406 print "If you didn't get a copy of the LICENSE contact the author at:\n"
407 print " zedshaw@zedshaw.com"
408 print ""
409 print "Have fun."
410