├── LICENSE.txt ├── Makefile ├── README.md ├── contrib ├── debian │ └── DEBIAN │ │ ├── cfgtrack.manpages │ │ ├── conffiles │ │ └── control └── release_Makefile └── src ├── cfgtrack ├── cfgtrack.1 └── cfgtrack_mail /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Ferry Boender 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROG=cfgtrack 2 | 3 | install: 4 | cp src/cfgtrack /usr/bin/cfgtrack 5 | cp src/cfgtrack_mail /usr/bin/cfgtrack_mail 6 | gzip -c src/cfgtrack.1 > /usr/share/man/man1/cfgtrack.1.gz 7 | 8 | uninstall: 9 | rm /usr/share/man/man1/cfgtrack.1.gz 10 | rm /usr/bin/cfgtrack_mail 11 | rm /usr/bin/cfgtrack 12 | echo "To remove configuration for cfgtrack, rm -rf /var/lib/cfgtrack/" 13 | 14 | release: release_src release_deb release_rpm 15 | 16 | release_src: 17 | @echo "Making release for version $(REL_VERSION)" 18 | 19 | @if [ -z "$(REL_VERSION)" ]; then echo "REL_VERSION required"; exit 1; fi 20 | 21 | # Prepare source 22 | rm -rf $(PROG)-$(REL_VERSION) 23 | mkdir $(PROG)-$(REL_VERSION) 24 | cp src/cfgtrack $(PROG)-$(REL_VERSION)/ 25 | cp src/cfgtrack_mail $(PROG)-$(REL_VERSION)/ 26 | cp src/cfgtrack.1 $(PROG)-$(REL_VERSION)/ 27 | cp LICENSE.txt $(PROG)-$(REL_VERSION)/ 28 | cp README.md $(PROG)-$(REL_VERSION)/ 29 | cp contrib/release_Makefile $(PROG)-$(REL_VERSION)/Makefile 30 | 31 | # Bump version numbers 32 | find $(PROG)-$(REL_VERSION)/ -type f -print0 | xargs -0 sed -i "s/%%VERSION%%/$(REL_VERSION)/g" 33 | 34 | # Create archives 35 | zip -r $(PROG)-$(REL_VERSION).zip $(PROG)-$(REL_VERSION) 36 | tar -vczf $(PROG)-$(REL_VERSION).tar.gz $(PROG)-$(REL_VERSION) 37 | 38 | # Cleanup 39 | rm -rf $(PROG)-$(REL_VERSION) 40 | 41 | release_deb: 42 | @if [ -z "$(REL_VERSION)" ]; then echo "REL_VERSION required"; exit 1; fi 43 | 44 | mkdir -p rel_deb/usr/bin 45 | mkdir -p rel_deb/usr/share/doc/$(PROG) 46 | mkdir -p rel_deb/usr/share/man/man1 47 | 48 | # Copy the source to the release directory structure. 49 | cp LICENSE.txt rel_deb/usr/share/doc/$(PROG) 50 | cp README.md rel_deb/usr/share/doc/$(PROG) 51 | cp src/$(PROG) rel_deb/usr/bin/$(PROG) 52 | cp src/$(PROG).1 rel_deb/usr/share/man/man1 53 | cp src/cfgtrack_mail rel_deb/usr/bin/ 54 | cp -ar contrib/debian/DEBIAN rel_deb/ 55 | 56 | # Bump version numbers 57 | find rel_deb/ -type f -print0 | xargs -0 sed -i "s/%%VERSION%%/$(REL_VERSION)/g" 58 | 59 | # Create debian pacakge 60 | fakeroot dpkg-deb --build rel_deb > /dev/null 61 | mv rel_deb.deb $(PROG)-$(REL_VERSION).deb 62 | 63 | # Cleanup 64 | rm -rf rel_deb 65 | 66 | release_rpm: release_deb 67 | alien -r -g -v cfgtrack-$(REL_VERSION).deb 68 | sed -i 's#%dir "/"##' cfgtrack-$(REL_VERSION)/cfgtrack-$(REL_VERSION)-2.spec 69 | sed -i 's#%dir "/usr/bin/"##' cfgtrack-$(REL_VERSION)/cfgtrack-$(REL_VERSION)-2.spec 70 | cd cfgtrack-$(REL_VERSION) && rpmbuild --target=noarch --buildroot $(shell readlink -f cfgtrack-$(REL_VERSION)) -bb cfgtrack-$(REL_VERSION)-2.spec 71 | 72 | clean: 73 | rm -rf *.tar.gz 74 | rm -rf *.zip 75 | rm -rf *.deb 76 | rm -rf rel_deb 77 | rm -rf cfgtrack-* 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cfgtrack 2 | ======== 3 | 4 | Download: https://github.com/fboender/cfgtrack/releases/ 5 | 6 | Packages available for: Debian, Ubuntu, Redhat, .tar.gz. 7 | 8 | 9 | About 10 | ----- 11 | 12 | cfgtrack tracks and reports diffs in files between invocations. 13 | 14 | It lets you add directories and files to a tracking list by keeping a separate 15 | copy of the file in a tracking directory. When invoked with the '`compare`' 16 | command, it outputs a Diff of any changes made in the files since the last 17 | time you invoked with the '`compare`' command. It then automatically updates the 18 | tracked file. It can also send an email with the diff attached. 19 | 20 | Limitations (otherwise known as "features") 21 | 22 | - Does not track file attribute changes (future?) 23 | - Does not track who made the change (future?) 24 | - Does not track additions to directories (but does track removals of tracked 25 | files) (future?). 26 | - It's not a configuration management system. 27 | - It's not an intrusion detection system. 28 | 29 | cfgtrack is useful when you want to stay informed about changes happening to 30 | files by third-parties or automatic updates. 31 | 32 | 33 | Example: 34 | 35 | [fboender@jib]~$ sudo ./cfgtrack track /etc/crontab 36 | Now tracking /etc/crontab 37 | 38 | # Two weeks later 39 | 40 | [fboender@jib]~$ sudo ./cfgtrack compare 41 | --- ./etc/crontab 2015-01-23 18:23:06.906938089 +0100 42 | +++ /./etc/crontab 2015-01-23 18:23:26.842937817 +0100 43 | @@ -12,6 +12,3 @@ 44 | 25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily ) 45 | 47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly ) 46 | 52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly ) 47 | -# MISSION CRITICAL: DO NOT REMOVE 48 | -00 3 1 * * root /usr/local/bin/fix_all_the_things.py 49 | - 50 | 51 | [fboender@jib]~$ echo "Oh god, why???" | mail -s "YOU'RE FIRED" --to admin@noobcorp.com 52 | 53 | 54 | Installation 55 | ------------ 56 | 57 | cfgtrack is written as a straight-forward portable shell script. No special 58 | installation is required other than downloading and copying the script to a 59 | directory. 60 | 61 | If you want to to use the mail function (-m/--mail), you'll need Python. 62 | 63 | ### Debian / Ubuntu / Linux Mint installation 64 | 65 | 1. Download the .deb file from https://github.com/fboender/cfgtrack/releases/ 66 | 2. Install the package: `sudo dpkg -i cfgtrack*.deb` 67 | 3. To uninstall: `apt-get purge cfgtrack` 68 | 69 | If you want to use the mail (-m) option, Python (v2.5+) must be installed. 70 | Python is available on nearly all unices. 71 | 72 | ### RedHat / Centos / RPM-based 73 | 74 | 1. Download the .rpm file from https://github.com/fboender/cfgtrack/releases/ 75 | 2. Install the package: `sudo rpm -i cfgtrack*.rpm` 76 | 77 | If you want to use the mail (-m) option, Python (v2.5+) must be installed. 78 | Python is available on nearly all unices. 79 | 80 | ### Manual install 81 | 82 | 1. Download the .tar.gz or .zip file from https://github.com/fboender/cfgtrack/releases/ 83 | 2. Unpack it with `tar -vxzf cfgtrack-*.tar.gz` 84 | 3. Change to the cfgtrack directory: `cd cfgtrack*` 85 | 4. Install it on your system with `sudo make install` 86 | 5. Uninstall it from your system with `sudo make uninstall` 87 | 88 | If you want to use the mail (-m) option, Python (v2.5+) must be installed. 89 | Python is available on nearly all unices. 90 | 91 | 92 | Usage 93 | ----- 94 | 95 | To start tracking an entire tree of files: 96 | 97 | $ sudo cfgtrack track /etc/ 98 | 99 | To track a single file: 100 | 101 | $ sudo cfgtrack track /etc/apt/apt.conf.d/50unattended-upgrades 102 | 103 | Note that cfgtrack keeps a copy of the files that are tracked in a separate 104 | directory. If you track large directories (which you really shouldn't), this 105 | will take up twice as much space. 106 | 107 | To show differences between the last time you ran 'compare' and now: 108 | 109 | $ sudo cfgtrack compare 110 | 111 | To send an email upon changes: 112 | 113 | $ sudo cfgtrack --silent --mail admin@example.com compare 114 | 115 | To stop tracking changes to an entire tree of files: 116 | 117 | $ sudo cfgtrack untrack /etc/ 118 | 119 | To stop tracking changes to a single file 120 | 121 | $ sudo cfgtrack untrack /etc/apt/apt.conf.d/50unattended-upgrade 122 | 123 | To get a daily report via email of changes and put the resulting diffs in the archive: 124 | 125 | $ vi /etc/cron.daily/cfgtrack 126 | #!/bin/sh 127 | 128 | /usr/bin/cfgtrack -a -s -m admin@foocorp.com compare >/dev/null 129 | 130 | $ chmod 755 /etc/cron.daily/cfgtrack 131 | 132 | 133 | See also `man 1 cfgtrack` 134 | -------------------------------------------------------------------------------- /contrib/debian/DEBIAN/cfgtrack.manpages: -------------------------------------------------------------------------------- 1 | docs/cfgtrack.1 2 | -------------------------------------------------------------------------------- /contrib/debian/DEBIAN/conffiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fboender/cfgtrack/af6111fa685fc6f5934829dc35ad8c32cc834321/contrib/debian/DEBIAN/conffiles -------------------------------------------------------------------------------- /contrib/debian/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: cfgtrack 2 | Version: %%VERSION%% 3 | Maintainer: Ferry Boender 4 | Section: utils 5 | Priority: optional 6 | Architecture: all 7 | Depends: diffutils 8 | Description: Track and report diffs in files between invocations. 9 | cfgtrack lets you add directories and files to a tracking list by keeping a 10 | seperate copy of the file in a tracking directory. When invoked with the 11 | 'compare' command, it outputs a Diff of any changes made in the configuration 12 | file since the last time you invoked with the 'compare' command. It then 13 | automatically updates the tracked file. 14 | -------------------------------------------------------------------------------- /contrib/release_Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | cp cfgtrack /usr/bin/cfgtrack 3 | cp cfgtrack_mail /usr/bin/cfgtrack_mail 4 | gzip -c cfgtrack.1 > /usr/share/man/man1/cfgtrack.1.gz 5 | 6 | uninstall: 7 | rm /usr/share/man/man1/cfgtrack.1.gz 8 | rm /usr/bin/cfgtrack_mail 9 | rm /usr/bin/cfgtrack 10 | echo "To remove configuration for cfgtrack, rm -rf /var/lib/cfgtrack/" 11 | -------------------------------------------------------------------------------- /src/cfgtrack: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright (c) 2015 Ferry Boender 5 | # 6 | # Released under the MIT license: 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | TRACK_DIR=/var/lib/cfgtrack/tracking 27 | ARCHIVE_DIR=/var/lib/cfgtrack/archive 28 | VERSION="%%VERSION%%" 29 | AUTHOR="Ferry Boender " 30 | 31 | # 32 | # Usage 33 | # 34 | usage() { 35 | echo "cfgtrack - Track and report diffs in files between invocations\n" >&2 36 | echo "$0 list # List files being tracked" >&2 37 | echo "$0 track # Start tracking file(s in dir)" >&2 38 | echo "$0 untrack # Stop tracking file(s in dir)" >&2 39 | echo "$0 [-a] [-s] [-m ] compare # Compare tracked files" >&2 40 | echo >&2 41 | echo " -a, --archive Store diffs in an archive dir" >&2 42 | echo " -s, --silent Don't output diff to stdout" >&2 43 | echo " -m, --mail Send diffs to email address" >&2 44 | echo >&2 45 | echo "By $AUTHOR, version: $VERSION. Released under the MIT license." >&2 46 | exit 1 47 | } 48 | 49 | # 50 | # Write error to STDERR and exit 51 | # 52 | err() { 53 | echo $* >&2 54 | exit 2 55 | } 56 | 57 | # 58 | # List files and dirs being tracked 59 | # 60 | list() { 61 | cd $TRACK_DIR 62 | find . -type f | sed "s/^\.//" 63 | } 64 | 65 | # 66 | # Add a path or file to the tracking list 67 | # 68 | track() { 69 | P=$1 70 | if [ $(echo $P | head -c1) \!= '/' ]; then 71 | err "Path must be absolute (start with /)" 72 | fi 73 | cp -ar --parents $P $TRACK_DIR/ || exit $? 74 | echo "Now tracking $P" 75 | } 76 | 77 | # 78 | # Remove a path or file from the tracking list 79 | # 80 | untrack() { 81 | P=$1 82 | if [ $(echo $P | head -c1) \!= '/' ]; then 83 | err "Path must be absolute (start with /)" 84 | fi 85 | UNTRACK="${1#/}" # Strip off leading '/' to prevent disaster 86 | if [ -z "$UNTRACK" ]; then 87 | err "Empty untracking path specified" 88 | fi 89 | # $TRACK_DIR is checked to never be empty or '/', so this rm -rf should be 90 | # safe. 91 | rm -rf $TRACK_DIR/$UNTRACK 92 | echo "No longer tracking $P" 93 | } 94 | 95 | # 96 | # Output diff's for files and paths being tracked. 97 | # 98 | compare() { 99 | ARCHIVE=$1 100 | 101 | # Go through all the files being tracked and diff them with their original 102 | # version 103 | TMP_FILE=$(mktemp) 104 | PREV_DIR=$(pwd) 105 | cd $TRACK_DIR 106 | find . -type f | while read -r file; do 107 | diff -Nur "$file" "/$file" >> $TMP_FILE 108 | RET_CODE=$? 109 | if [ $RET_CODE -eq 1 ] || [ $RET_CODE -eq 2 ]; then 110 | # File has changed. Store the new file in the tracking dir. 111 | if [ \! -e "${file#.}" ]; then 112 | # File no longer exists, remove it from the tracking dir 113 | rm "$file" 114 | else 115 | cp -a "${file#.}" "$file" 116 | fi 117 | fi 118 | done; 119 | cd $PREV_DIR 120 | 121 | # Were there any changes? 122 | if [ -s $TMP_FILE ]; then 123 | if [ -n "$ARCHIVE" ]; then 124 | # Store the diff in the archive 125 | ARCHIVE_FILE=$(date +"%Y-%m-%d_%H:%M.diff") 126 | cp $TMP_FILE $ARCHIVE_DIR/$ARCHIVE_FILE 127 | fi 128 | 129 | if [ -n "$MAIL_TO" ]; then 130 | # Send the diff to email recipient(s) 131 | ATTACH_FNAME="$(hostname -f)_$(date +"%Y-%m-%d_%H:%M.diff")" 132 | cp $TMP_FILE /tmp/$ATTACH_FNAME 133 | BODY="cfgtrack running at $(hostname -f) has found changes in tracked files.\n\n" 134 | BODY="${BODY}Please see the attached file for a list of changes in Diff format\n" 135 | printf "$BODY" | $(dirname $0)/cfgtrack_mail -t $MAIL_TO -s "cfgtrack @ $(hostname -f): Changes found" /tmp/$ATTACH_FNAME 136 | rm /tmp/$ATTACH_FNAME 137 | fi 138 | 139 | if [ -z "$SILENT" ]; then 140 | # Not --silent 141 | cat $TMP_FILE 142 | fi 143 | fi 144 | rm $TMP_FILE 145 | } 146 | 147 | # Validate settings 148 | if [ $TRACK_DIR = '/' ]; then 149 | err "Tracking dir $TRACK_DIR cannot be /" 150 | fi 151 | if [ \! -d $TRACK_DIR ]; then 152 | mkdir -p $TRACK_DIR || err "Can't create tracking dir" 153 | fi 154 | if [ \! -d $ARCHIVE_DIR ]; then 155 | mkdir -p $ARCHIVE_DIR || err "Can't create archive dir" 156 | fi 157 | 158 | # Handle options 159 | while true ; do 160 | case "$1" in 161 | -a|--archive) 162 | ARCHIVE="1" 163 | shift 1 164 | ;; 165 | -s|--silent) 166 | SILENT="1" 167 | shift 1 168 | ;; 169 | -m|--mail) 170 | if [ \! -x "$(dirname $0)/cfgtrack_mail" ]; then 171 | err "cfgtrack_mail not found. Can't send email" 172 | fi 173 | MAIL_TO=$2 174 | shift 2 175 | ;; 176 | -h|--help) 177 | usage 178 | break 179 | ;; 180 | --) 181 | shift 1 182 | break 183 | ;; 184 | list) 185 | list 186 | exit $? 187 | break 188 | ;; 189 | track) 190 | if [ -z "$2" ]; then 191 | usage 192 | fi 193 | track $2 194 | exit $? 195 | break 196 | ;; 197 | untrack) 198 | if [ -z "$2" ]; then 199 | usage 200 | fi 201 | untrack $2 202 | exit $? 203 | break 204 | ;; 205 | compare) 206 | compare $ARCHIVE 207 | exit $? 208 | break 209 | ;; 210 | *) 211 | usage; 212 | break 213 | ;; 214 | esac 215 | done 216 | -------------------------------------------------------------------------------- /src/cfgtrack.1: -------------------------------------------------------------------------------- 1 | .\" Process this file with 2 | .\" groff -man -Tascii foo.1 3 | .\" 4 | .TH CFGTRACK 1 "JANUARI 2014" Linux "User Manuals" 5 | 6 | .SH NAME 7 | cfgtrack \- track and report diffs in files between invocations. 8 | .SH SYNOPSIS 9 | .B cfgtrack 10 | .I list 11 | 12 | .B cfgtrack 13 | .I track 14 | 15 | 16 | .B cfgtrack 17 | .I untrack 18 | 19 | 20 | .B cfgtrack 21 | [-s] [-a] [-s] [-m "[ ]"] 22 | .I compare 23 | 24 | .SH DESCRIPTION 25 | .B Cfgtrack 26 | lets you add files to a tracking list by keeping a seperate copy of the file in a tracking directory. When invoked with the 'compare' command, it outputs a Diff of any changes made in the configuration file since the last time you invoked with the 'compare' command. It then automatically updates the tracked file. 27 | 28 | .SH OPTIONS 29 | .IP "list" 30 | List the files currently being tracked. 31 | .IP "track " 32 | Add to the list of files to be tracked. must be an absolute path to a file or directory. If is a directory, all files under that directory will be tracked recursively. 33 | .IP "untrack " 34 | Remove from the list of files to be tracked. must be an absolute path to a file or directory. 35 | .IP "compare" 36 | Show the difference (in diff(1) format) in files being tracked since the last time "compare" was called. 37 | .IP "-s, --silent" 38 | Do not output the diff on the console. 39 | .IP "-a, --archive" 40 | Store the diff in the archive directory, if there were any changes. 41 | .IP "-m, --mail " 42 | In case of changes in tracked files, send an email to . may be multiple address, in which case they must be separated by a space and the whole list must be enclosed in quotes. 43 | .IP "-h, --help" 44 | Show this help message and exit 45 | 46 | .SH EXAMPLES 47 | .B cfgtrack track /etc/apache/ 48 | .RS 49 | Start tracking all the files under /etc/apache/ 50 | .RE 51 | 52 | .B cfgtrack untrack /etc/apache/ports.conf.dpkg-dist 53 | .RS 54 | Stop tracking changes to the file /etc/apache/ports.conf.dpkg-dist 55 | .RE 56 | 57 | .B cfgtrack -a -m "admin@example.com" compare 58 | .RS 59 | Compare all the tracked files and, if differences are found, create an archive file with those differences in the archive directory and show the differences on STDOUT. Also send an email with the changes to admin@example.com 60 | .RE 61 | 62 | .SH LIMITATIONS 63 | 64 | cfgtrack does not track file attribute changes. 65 | 66 | cfgtrack does not track new files in directories. It only tracks files that existed in directories when they were added. It does track removals of files. 67 | 68 | cfgtrack does not track which user made the change. 69 | 70 | cfgtrack's mail option requires a locally running SMTP server. 71 | 72 | 73 | .SH FILES 74 | .I /var/lib/cfgtrack/tracked 75 | .RS 76 | Location where copies of the tracked files are stored for comparison. 77 | .RE 78 | 79 | .I /var/lib/cfgtrack/archive 80 | .RS 81 | Location for the archived diff files. 82 | .RE 83 | 84 | .SH COPYRIGHT 85 | Copyright 2015 Ferry Boender 86 | 87 | cfgtrack is released under the MIT License. 88 | -------------------------------------------------------------------------------- /src/cfgtrack_mail: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | import select 6 | import optparse 7 | import smtplib 8 | import mimetypes 9 | from optparse import OptionParser 10 | from email import encoders 11 | from email.message import Message 12 | from email.mime.audio import MIMEAudio 13 | from email.mime.base import MIMEBase 14 | from email.mime.image import MIMEImage 15 | from email.mime.multipart import MIMEMultipart 16 | from email.mime.text import MIMEText 17 | 18 | def send_mail(send_from, send_to, subject, text, files=None, 19 | server="127.0.0.1"): 20 | # Create the enclosing (outer) message 21 | outer = MIMEMultipart() 22 | outer['Subject'] = subject 23 | outer['To'] = ', '.join(send_to) 24 | outer['From'] = send_from 25 | outer.preamble = 'Your email reader is not MIME-aware\n' 26 | 27 | first_msg = MIMEText(text) 28 | outer.attach(first_msg) 29 | 30 | for path in files: 31 | # Guess the content type based on the file's extension. Encoding 32 | # will be ignored, although we should check for simple things like 33 | # gzip'd or compressed files. 34 | ctype, encoding = mimetypes.guess_type(path) 35 | if ctype is None or encoding is not None: 36 | # No guess could be made, or the file is encoded (compressed), so 37 | # use a generic bag-of-bits type. 38 | ctype = 'application/octet-stream' 39 | maintype, subtype = ctype.split('/', 1) 40 | if maintype == 'text': 41 | fp = open(path) 42 | # Note: we should handle calculating the charset 43 | msg = MIMEText(fp.read(), _subtype=subtype) 44 | fp.close() 45 | elif maintype == 'image': 46 | fp = open(path, 'rb') 47 | msg = MIMEImage(fp.read(), _subtype=subtype) 48 | fp.close() 49 | elif maintype == 'audio': 50 | fp = open(path, 'rb') 51 | msg = MIMEAudio(fp.read(), _subtype=subtype) 52 | fp.close() 53 | else: 54 | fp = open(path, 'rb') 55 | msg = MIMEBase(maintype, subtype) 56 | msg.set_payload(fp.read()) 57 | fp.close() 58 | # Encode the payload using Base64 59 | encoders.encode_base64(msg) 60 | # Set the filename parameter 61 | filename = os.path.basename(path) 62 | msg.add_header('Content-Disposition', 'attachment', filename=filename) 63 | outer.attach(msg) 64 | 65 | # Now send or store the message 66 | composed = outer.as_string() 67 | s = smtplib.SMTP(server) 68 | s.sendmail(send_from, send_to, composed) 69 | s.quit() 70 | 71 | def error(msg): 72 | sys.stderr.write(msg + '\n') 73 | sys.exit(1) 74 | 75 | if __name__ == '__main__': 76 | usage = "usage: echo 'Body' | %prog [options] -t [attachment] [..]" 77 | parser = optparse.OptionParser(usage) 78 | parser.add_option("-f", "--from", action="store", dest="from_", default=None, help="Send mail from address") 79 | parser.add_option("-t", "--to", action="store", dest="to", default=None, help="Comma-separated list of to addresses") 80 | parser.add_option("-s", "--subject", action="store", dest="subject", default="", help="Subject of the email") 81 | parser.add_option( "--host", action="store", dest="smtp_host", default="127.0.0.1", help="SMTP server") 82 | (options, args) = parser.parse_args() 83 | 84 | if not select.select([sys.stdin,],[],[],0.0)[0]: 85 | error("Specify a body by piping it into stdin") 86 | if not options.to: 87 | error("Must specify one or more recipients with -t/--to") 88 | if not options.from_: 89 | import socket 90 | import getpass 91 | options.from_ = '%s@%s' % (getpass.getuser(), socket.getfqdn()) 92 | 93 | options.to = options.to.split(',') 94 | body = sys.stdin.read() 95 | 96 | send_mail(options.from_, 97 | options.to, 98 | options.subject, 99 | body, 100 | files=args, 101 | server=options.smtp_host) 102 | --------------------------------------------------------------------------------