├── Dockerfile ├── LICENSE ├── README.md ├── circle.yml ├── container-files ├── config │ └── init │ │ └── bootstrap-m12.sh └── usr │ ├── bin │ └── sendmail │ └── local │ └── share │ └── zabbix │ ├── alertscripts │ ├── zabbix_notifications.sh │ ├── zabbix_sendmail.sh │ └── zabbix_slack.sh │ └── externalscripts │ ├── getFPMInfo.py │ └── getNginxInfo.py ├── docker-cloud.yml ├── images ├── actions1.jpg ├── actions2.jpg ├── media-type.jpg ├── nginx1.jpg ├── nginx2.jpg ├── php-fpm-stats.jpg └── user-media.jpg └── templates-files ├── zabbix-php-fpm-template.xml └── zbx_nginx_template.xml /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM zabbix/zabbix-3.0:3.0.1 2 | MAINTAINER Marcin Ryzycki marcin@m12.io, Przemyslaw Ozgo linux@ozgo.info 3 | 4 | RUN \ 5 | rpm --rebuilddb && yum clean all && \ 6 | yum install --nogpgcheck -y gcc make && \ 7 | easy_install simplejson && \ 8 | rpm -e --nodeps make gcc 9 | 10 | 11 | ENV \ 12 | ZABBIX_ADMIN_EMAIL=default@domain.com \ 13 | ZABBIX_SMTP_SERVER=default.smtp.server.com \ 14 | ZABBIX_SMTP_USER=default.smtp.username \ 15 | ZABBIX_SMTP_PASS=default.smtp.password \ 16 | SLACK_WEBHOOK=SLACK_WEBHOOK 17 | 18 | COPY container-files / 19 | RUN mv /usr/local/etc/web/zabbix.conf.php /usr/local/src/zabbix/frontends/php/conf/zabbix.conf.php 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015 MILLION12 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Zabbix-Server (CentOS-7 + Supervisor) 2 | 3 | [![CircleCI Build Status](https://img.shields.io/circleci/project/million12/docker-zabbix-server/master.svg)](https://circleci.com/gh/million12/docker-zabbix-server/tree/master) 4 | [![GitHub Open Issues](https://img.shields.io/github/issues/million12/docker-zabbix-server.svg)](https://github.com/million12/docker-zabbix-server/issues) 5 | [![GitHub Stars](https://img.shields.io/github/stars/million12/docker-zabbix-server.svg)](https://github.com/million12/docker-zabbix-server) 6 | [![GitHub Forks](https://img.shields.io/github/forks/million12/docker-zabbix-server.svg)](https://github.com/million12/docker-zabbix-server) 7 | [![Stars on Docker Hub](https://img.shields.io/docker/stars/million12/zabbix-server.svg)](https://hub.docker.com/r/million12/zabbix-server) 8 | [![Pulls on Docker Hub](https://img.shields.io/docker/pulls/million12/zabbix-server.svg)](https://hub.docker.com/r/million12/zabbix-server) 9 | [![Docker Layers](https://badge.imagelayers.io/million12/zabbix-server:latest.svg)](https://hub.docker.com/r/million12/zabbix-server) 10 | 11 | [![Deploy to Docker Cloud](https://files.cloud.docker.com/images/deploy-to-dockercloud.svg)](https://cloud.docker.com/stack/deploy/?repo=https://github.com/million12/docker-zabbix-server/tree/master) 12 | 13 | [Docker Image](https://registry.hub.docker.com/u/million12/zabbix-server/) with Zabbix Server using CentOS-7 and Supervisor. 14 | Image is using external datbase. 15 | This image is using offcial [zabbix-server-2.4](https://registry.hub.docker.com/u/zabbix/zabbix-server-2.4/) Docker Image as base image. 16 | 17 | #### Installed Plugins 18 | > Email Notifications with authorisation 19 | > Slack Notifications 20 | > Nginx Status `/nginx_status` 21 | > PHP-FPM Status `/fpm_status` 22 | 23 | ### Database deployment 24 | To be able to connect to database we would need one to be running first. Easiest way to do that is to use another docker image. For this purpose we will use our [million12/mariadb](https://registry.hub.docker.com/u/million12/mariadb/) image as our database. 25 | 26 | **For more information about million12/MariaDB see our [documentation.](https://github.com/million12/docker-mariadb) ** 27 | 28 | Example: 29 | 30 | docker run \ 31 | -d \ 32 | --name zabbix-db \ 33 | -p 3306:3306 \ 34 | --env="MARIADB_USER=username" \ 35 | --env="MARIADB_PASS=my_password" \ 36 | million12/mariadb 37 | 38 | ***Remember to use the same credentials when deploying zabbix-server image.*** 39 | 40 | 41 | ### Environmental Variable 42 | IYou can use environmental variables to config Zabbix server and PHP. Available 43 | variables: 44 | 45 | | Variable | Default value | 46 | | -------- | ------------- | 47 | | PHP_date_timezone | UTC | 48 | | PHP_max_execution_time | 300 | 49 | | PHP_max_input_time | 300 | 50 | | PHP_memory_limit | 128M | 51 | | PHP_error_reporting | E_ALL | 52 | | ZS_ListenPort | 10051 | 53 | | ZS_SourceIP | | 54 | | ZS_LogFile | /tmp/zabbix_server.log | 55 | | ZS_LogFileSize | 10 | 56 | | ZS_DebugLevel | 3 | 57 | | ZS_PidFile | /var/run/zabbix_server.pid | 58 | | ZS_DBHost | zabbix.db | 59 | | ZS_DBName | zabbix | 60 | | ZS_DBSchema | | 61 | | ZS_DBUser | zabbix | 62 | | ZS_DBPassword | zabbix | 63 | | ZS_DBSocket | /tmp/mysql.sock | 64 | | ZS_DBPort | 3306 | 65 | | ZS_StartPollers | 5 | 66 | | ZS_StartPollersUnreachable | 1 | 67 | | ZS_StartTrappers | 5 | 68 | | ZS_StartPingers | 1 | 69 | | ZS_StartDiscoverers | 1 | 70 | | ZS_StartHTTPPollers | 1 | 71 | | ZS_StartTimers | 1 | 72 | | ZS_JavaGateway | 127.0.0.1 | 73 | | ZS_JavaGatewayPort | 10052 | 74 | | ZS_StartJavaPollers | 0 | 75 | | ZS_StartVMwareCollectors | 0 | 76 | | ZS_VMwareFrequency | 60 | 77 | | ZS_VMwarePerfFrequency | 60 | 78 | | ZS_VMwareCacheSize | 8M | 79 | | ZS_VMwareTimeout | 10 | 80 | | ZS_SNMPTrapperFile | /tmp/zabbix_traps.tmp | 81 | | ZS_StartSNMPTrapper | 0 | 82 | | ZS_ListenIP | 0.0.0.0 | 83 | | ZS_HousekeepingFrequency | 1 | 84 | | ZS_MaxHousekeeperDelete | 500 | 85 | | ZS_SenderFrequency | 30 | 86 | | ZS_CacheSize | 8M | 87 | | ZS_CacheUpdateFrequency | 60 | 88 | | ZS_StartDBSyncers | 4 | 89 | | ZS_HistoryCacheSize | 8M | 90 | | ZS_TrendCacheSize | 4M | 91 | | ZS_HistoryTextCacheSize | 16M | 92 | | ZS_ValueCacheSize | 8M | 93 | | ZS_Timeout | 3 | 94 | | ZS_TrapperTimeout | 300 | 95 | | ZS_UnreachablePeriod | 45 | 96 | | ZS_UnavailableDelay | 60 | 97 | | ZS_UnreachableDelay | 15 | 98 | | ZS_AlertScriptsPath | /usr/local/share/zabbix/alertscripts | 99 | | ZS_ExternalScripts | /usr/local/share/zabbix/externalscripts | 100 | | ZS_FpingLocation | /usr/sbin/fping | 101 | | ZS_Fping6Location | /usr/sbin/fping6 | 102 | | ZS_SSHKeyLocation | | 103 | | ZS_LogSlowQueries | 0 | 104 | | ZS_TmpDir | /tmp | 105 | | ZS_StartProxyPollers | 1 | 106 | | ZS_ProxyConfigFrequency | 3600 | 107 | | ZS_ProxyDataFrequency | 1 | 108 | | ZS_AllowRoot | 0 | 109 | | ZS_User | zabbix | 110 | | ZS_Include | | 111 | | ZS_SSLCertLocation | /usr/local/share/zabbix/ssl/certs | 112 | | ZS_SSLKeyLocation | /usr/local/share/zabbix/ssl/keys | 113 | | ZS_SSLCALocation | | 114 | | ZS_LoadModulePath | /usr/lib/zabbix/modules | 115 | | ZS_LoadModule | | 116 | 117 | #### Configuration from volume 118 | Full config files can be also used. Environment configs will be overriden by 119 | values from config files in this case. You need only to add */etc/custom-config/* 120 | volume: 121 | 122 | ``` 123 | -v /host/custom-config/:/etc/custom-config/ 124 | ``` 125 | 126 | Available files: 127 | 128 | | File | Description | 129 | | ---- | ----------- | 130 | | php-zabbix.ini | PHP configuration file | 131 | | zabbix_server.conf | Zabbix server configuration file | 132 | 133 | 134 | ### Zabbix-Server Deployment 135 | Now when we have our database running we can deploy zabbix-server image with appropriate environmental variables set. 136 | 137 | Example: 138 | 139 | docker run \ 140 | -d \ 141 | --name zabbix \ 142 | -p 80:80 \ 143 | -p 10051:10051 \ 144 | --link zabbix-db:zabbix.db \ 145 | --env="ZS_DBHost=zabbix.db" \ 146 | --env="ZS_DBUser=zabbix" \ 147 | --env="ZS_DBPassword=my_password" \ 148 | million12/zabbix-server 149 | With email settings and Slack integration and custom timezone: 150 | 151 | docker run \ 152 | -d \ 153 | --name zabbix \ 154 | -p 80:80 \ 155 | -p 10051:10051 \ 156 | --link zabbix-db:zabbix.db \ 157 | --env="ZS_DBHost=zabbix.db" \ 158 | --env="ZS_DBUser=zabbix" \ 159 | --env="ZS_DBPassword=my_password" \ 160 | --env="ZABBIX_ADMIN_EMAIL=default@domain.com" \ 161 | --env="ZABBIX_SMTP_SERVER=default.smtp.server.com" \ 162 | --env="ZABBIX_SMTP_USER=default.smtp.username" \ 163 | --env="ZABBIX_SMTP_PASS=default.smtp.password" \ 164 | --env="SLACK_WEBHOOK=https://hooks.slack.com/services/QQ3PTH/B67THC0D3/ABCDGabcDEF124" \ 165 | million12/zabbix-server 166 | 167 | With MySQL/MariaDB linked container: 168 | 169 | ~~~ 170 | docker run \ 171 | -d \ 172 | --name zabbix \ 173 | -p 80:80 \ 174 | -p 10051:10051 \ 175 | --link some-mariadb:db \ 176 | million12/zabbix-server 177 | ~~~ 178 | 179 | ### Access Zabbix-Server web interface 180 | To log in into zabbix web interface for the first time use credentials 181 | `Admin:zabbix`. 182 | 183 | Access web interface under [http://docker_host_ip]() 184 | Follow the on screen instructions. 185 | 186 | ### Zabbix Push Notifications 187 | Zabbix notification script is located in `/usr/local/share/zabbix/alertscripts` 188 | Please follow [Zabbkit manual](https://www.zabbix.com/forum/showthread.php?t=41967) to configure notifications. 189 | 190 | ### Email Notifications (Server settings) 191 | Using environmental variables on `docker run` you can edit default email server settings. Valuse would be added on into `/usr/local/share/zabbix/alertscripts/zabbix_sendmail.sh`. 192 | Environmental variables are: 193 | `ZABBIX_ADMIN_EMAIL=default@domain.com` 194 | `ZABBIX_SMTP_SERVER=default.smtp.server.com` 195 | `ZABBIX_SMTP_USER=default.smtp.username` 196 | `ZABBIX_SMTP_PASS=default.smtp.password` 197 | 198 | Configuration: 199 | Go into `Administration/Media types` and add new type using `script` as Type. Script name should be `zabbix_sendmail.sh` 200 | 201 | ![Media type](https://raw.githubusercontent.com/million12/docker-zabbix-server/master/images/media-type.jpg) 202 | 203 | Next go to `Configuration/Actions` and create new action. Select recovery message. 204 | 205 | ![Actions1](https://raw.githubusercontent.com/million12/docker-zabbix-server/master/images/actions1.jpg) 206 | 207 | Next select tab `Operations` and click New to add new action. In `Send to User groups` add Zabbix Adminstrator or any user group you like. In `Send only to` select Name of your previously created `Media type` name. 208 | 209 | ![Actions2](https://raw.githubusercontent.com/million12/docker-zabbix-server/master/images/actions2.jpg) 210 | 211 | Next go to `Administration/Users` and select your user. Go to Media tab and add new Media. In `Type` select your `Media type` you have created in first step. Add your email addess and enjoy receiving emails. 212 | 213 | ![User-Media](https://raw.githubusercontent.com/million12/docker-zabbix-server/master/images/user-media.jpg) 214 | 215 | ### Slack Integration 216 | This docker image comes with Slack integrations script. You need to provide your `WebHook` generated in yo your Slack account. it should look like `https://hooks.slack.com/services/QQ3PTH/B67THC0D3/ABCDGabcDEF124` 217 | 218 | Note: this Slack Integration script is based on one originally developed by [ericoc/zabbix-slack-alertscript](https://github.com/ericoc/zabbix-slack-alertscript). 219 | For deatiled installation please see [ericoc instructions](https://github.com/ericoc/zabbix-slack-alertscript). 220 | 221 | ### Nginx Status Template 222 | Nginx stats script is located in `/usr/local/share/zabbix/externalscripts/`. 223 | It's latest version of [vicendominguez/nginx-zabbix-template](https://github.com/vicendominguez/nginx-zabbix-template) official Zabbix communty repo. 224 | Gathering data is done by `getNginxInfo.py` which is already installed in this image. User need to install template to be able to use this feature. 225 | Go to `Configuration/Templates` and select `Import` and import file located in this repo in `templates-files` directory called `zbx_nginx_template.xml` 226 | 227 | ![Nginx1](https://raw.githubusercontent.com/million12/docker-zabbix-server/master/images/nginx1.jpg) 228 | 229 | ![Nginx2](https://raw.githubusercontent.com/million12/docker-zabbix-server/master/images/nginx2.jpg) 230 | 231 | More details in official documentation [here](https://github.com/vicendominguez/nginx-zabbix-template). 232 | 233 | ### PHP-FPM Status Template 234 | PHP-FPM Status template assumes that your Nginx server is configured to serve stats under `/fpm_status` url and port 80. If you you want to use custom port please edit necessary files. 235 | 236 | Example of `nginx.conf` file: 237 | 238 | location /fpm_status { 239 | access_log off; 240 | allow 127.0.0.1; 241 | deny all; 242 | include fastcgi_params; 243 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 244 | fastcgi_pass php-upstream; 245 | } 246 | 247 | Installation: 248 | Go to Configuration/Templates and select Import and import file located in this repo in templates-files directory called `zabbix-php-fpm-template.xml` 249 | 250 | Screenshots: 251 | ![PHP-FPM](https://raw.githubusercontent.com/million12/docker-zabbix-server/master/images/php-fpm-stats.jpg) 252 | 253 | Docker troubleshooting 254 | ====================== 255 | 256 | Use docker command to see if all required containers are up and running: 257 | ``` 258 | $ docker ps 259 | ``` 260 | 261 | Check logs of Zabbix server container: 262 | ``` 263 | $ docker logs zabbix 264 | ``` 265 | 266 | Sometimes you might just want to review how things are deployed inside a running 267 | container, you can do this by executing a _bash shell_ through _docker's 268 | exec_ command: 269 | ``` 270 | docker exec -ti zabbix /bin/bash 271 | ``` 272 | 273 | History of an image and size of layers: 274 | ``` 275 | docker history --no-trunc=true million12/zabbix-server | tr -s ' ' | tail -n+2 | awk -F " ago " '{print $2}' 276 | ``` 277 | 278 | ## Author 279 | 280 | Author: Przemyslaw Ozgo () 281 | 282 | --- 283 | 284 | **Sponsored by** [Typostrap.io - the new prototyping tool](http://typostrap.io/) for building highly-interactive prototypes of your website or web app. Built on top of TYPO3 Neos CMS and Zurb Foundation framework. 285 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | services: 3 | - docker 4 | environment: 5 | DB_USER: db-user 6 | DB_PASS: db-password 7 | 8 | dependencies: 9 | pre: 10 | - docker pull million12/mariadb 11 | post: 12 | # Launch DB backend 13 | - docker run -d --name=db --env="MARIADB_USER=$DB_USER" --env="MARIADB_PASS=$DB_PASS" million12/mariadb 14 | - docker logs -f db | tee -a ${CIRCLE_ARTIFACTS}/db.log: 15 | background: true 16 | - docker build -t million12/zabbix-server-base:test . 17 | 18 | test: 19 | override: 20 | - while true; do if grep "You can now connect to this MariaDB Server" -a ${CIRCLE_ARTIFACTS}/db.log; then break; else sleep 1; fi done 21 | - docker run -d --name zabbix --link db:zabbix.db -p 80:80 -p 10051:10051 --env="ZS_DBHost=zabbix.db" --env="ZS_DBUser=$DB_USER" --env="ZS_DBPassword=$DB_PASS" million12/zabbix-server-base:test 22 | - docker logs -f zabbix | tee -a ${CIRCLE_ARTIFACTS}/zabbix.log: 23 | background: true 24 | - while true; do if grep "nginx entered RUNNING state" -a ${CIRCLE_ARTIFACTS}/zabbix.log; then break; else sleep 1; fi done 25 | # Check Zabbix Web interface status 26 | - curl -s -L --head http://127.0.0.1/ | grep "HTTP/1.1 200 OK" 27 | -------------------------------------------------------------------------------- /container-files/config/init/bootstrap-m12.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | export TERM=xterm 4 | # Bash Colors 5 | green=`tput setaf 2` 6 | bold=`tput bold` 7 | reset=`tput sgr0` 8 | # Logging Functions 9 | log() { 10 | if [[ "$@" ]]; then echo "${bold}${green}[LOG `date +'%T'`]${reset} $@"; 11 | else echo; fi 12 | } 13 | email_setup() { 14 | sed -i 's/default@domain.com/'${ZABBIX_ADMIN_EMAIL}'/g' /usr/local/share/zabbix/alertscripts/zabbix_sendmail.sh 15 | sed -i 's/default.smtp.server.com/'${ZABBIX_SMTP_SERVER}'/g' /usr/local/share/zabbix/alertscripts/zabbix_sendmail.sh 16 | sed -i 's/default.smtp.username/'${ZABBIX_SMTP_USER}'/g' /usr/local/share/zabbix/alertscripts/zabbix_sendmail.sh 17 | sed -i 's/default.smtp.password/'${ZABBIX_SMTP_PASS}'/g' /usr/local/share/zabbix/alertscripts/zabbix_sendmail.sh 18 | } 19 | slack_webhook() { 20 | sed -i 's#SLACK_WEBHOOK#'$SLACK_WEBHOOK'#g' /usr/local/share/zabbix/alertscripts/zabbix_slack.sh 21 | } 22 | ####################### End of default settings ####################### 23 | # Zabbix default sql files 24 | log "Editing Admin Email Server Settings" 25 | email_setup 26 | log "Email server settings updated." 27 | log "Adding Slack integration and webhook provided by the user" 28 | slack_webhook 29 | log "Slack configuration updated" 30 | #zabbix_agentd -c /usr/local/etc/zabbix_agentd.conf 31 | -------------------------------------------------------------------------------- /container-files/usr/bin/sendmail: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | ############################################################################## 3 | ## sendEmail 4 | ## Written by: Brandon Zehm 5 | ## 6 | ## License: 7 | ## sendEmail (hereafter referred to as "program") is free software; 8 | ## you can redistribute it and/or modify it under the terms of the GNU General 9 | ## Public License as published by the Free Software Foundation; either version 10 | ## 2 of the License, or (at your option) any later version. 11 | ## When redistributing modified versions of this source code it is recommended 12 | ## that that this disclaimer and the above coder's names are included in the 13 | ## modified code. 14 | ## 15 | ## Disclaimer: 16 | ## This program is provided with no warranty of any kind, either expressed or 17 | ## implied. It is the responsibility of the user (you) to fully research and 18 | ## comprehend the usage of this program. As with any tool, it can be misused, 19 | ## either intentionally (you're a vandal) or unintentionally (you're a moron). 20 | ## THE AUTHOR(S) IS(ARE) NOT RESPONSIBLE FOR ANYTHING YOU DO WITH THIS PROGRAM 21 | ## or anything that happens because of your use (or misuse) of this program, 22 | ## including but not limited to anything you, your lawyers, or anyone else 23 | ## can dream up. And now, a relevant quote directly from the GPL: 24 | ## 25 | ## NO WARRANTY 26 | ## 27 | ## 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 28 | ## FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 29 | ## OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 30 | ## PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 31 | ## OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 32 | ## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 33 | ## TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 34 | ## PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 35 | ## REPAIR OR CORRECTION. 36 | ## 37 | ############################################################################## 38 | use strict; 39 | use IO::Socket; 40 | 41 | 42 | ######################## 43 | ## Global Variables ## 44 | ######################## 45 | 46 | my %conf = ( 47 | ## General 48 | "programName" => $0, ## The name of this program 49 | "version" => '1.56', ## The version of this program 50 | "authorName" => 'Brandon Zehm', ## Author's Name 51 | "authorEmail" => 'caspian@dotconf.net', ## Author's Email Address 52 | "timezone" => '+0000', ## We always use +0000 for the time zone 53 | "hostname" => 'changeme', ## Used in printmsg() for all output (is updated later in the script). 54 | "debug" => 0, ## Default debug level 55 | "error" => '', ## Error messages will often be stored here 56 | 57 | ## Logging 58 | "stdout" => 1, 59 | "logging" => 0, ## If this is true the printmsg function prints to the log file 60 | "logFile" => '', ## If this is specified (form the command line via -l) this file will be used for logging. 61 | 62 | ## Network 63 | "server" => 'localhost', ## Default SMTP server 64 | "port" => 25, ## Default port 65 | "bindaddr" => '', ## Default local bind address 66 | "alarm" => '', ## Default timeout for connects and reads, this gets set from $opt{'timeout'} 67 | "tls_client" => 0, ## If TLS is supported by the client (us) 68 | "tls_server" => 0, ## If TLS is supported by the remote SMTP server 69 | 70 | ## Email 71 | "delimiter" => "----MIME delimiter for sendEmail-" ## MIME Delimiter 72 | . rand(1000000), ## Add some randomness to the delimiter 73 | "Message-ID" => rand(1000000) . "-sendEmail", ## Message-ID for email header 74 | 75 | ); 76 | 77 | 78 | ## This hash stores the options passed on the command line via the -o option. 79 | my %opt = ( 80 | ## Addressing 81 | "reply-to" => '', ## Reply-To field 82 | 83 | ## Message 84 | "message-file" => '', ## File to read message body from 85 | "message-header" => '', ## Additional email header line(s) 86 | "message-format" => 'normal', ## If "raw" is specified the message is sent unmodified 87 | "message-charset" => 'iso-8859-1', ## Message character-set 88 | "message-content-type" => 'auto', ## auto, text, html or an actual string to put into the content-type header. 89 | 90 | ## Network 91 | "timeout" => 60, ## Default timeout for connects and reads, this is copied to $conf{'alarm'} later. 92 | "fqdn" => 'changeme', ## FQDN of this machine, used during SMTP communication (is updated later in the script). 93 | 94 | ## eSMTP 95 | "username" => '', ## Username used in SMTP Auth 96 | "password" => '', ## Password used in SMTP Auth 97 | "tls" => 'auto', ## Enable or disable TLS support. Options: auto, yes, no 98 | 99 | ); 100 | 101 | ## More variables used later in the program 102 | my $SERVER; 103 | my $CRLF = "\015\012"; 104 | my $subject = ''; 105 | my $header = ''; 106 | my $message = ''; 107 | my $from = ''; 108 | my @to = (); 109 | my @cc = (); 110 | my @bcc = (); 111 | my @attachments = (); 112 | my @attachments_names = (); 113 | 114 | ## For printing colors to the console 115 | my ${colorRed} = "\033[31;1m"; 116 | my ${colorGreen} = "\033[32;1m"; 117 | my ${colorCyan} = "\033[36;1m"; 118 | my ${colorWhite} = "\033[37;1m"; 119 | my ${colorNormal} = "\033[m"; 120 | my ${colorBold} = "\033[1m"; 121 | my ${colorNoBold} = "\033[0m"; 122 | 123 | ## Don't use shell escape codes on Windows systems 124 | if ($^O =~ /win/i) { 125 | ${colorRed} = ${colorGreen} = ${colorCyan} = ${colorWhite} = ${colorNormal} = ${colorBold} = ${colorNoBold} = ""; 126 | } 127 | 128 | ## Load IO::Socket::SSL if it's available 129 | eval { require IO::Socket::SSL; }; 130 | if ($@) { $conf{'tls_client'} = 0; } 131 | else { $conf{'tls_client'} = 1; } 132 | 133 | 134 | 135 | 136 | 137 | 138 | ############################# 139 | ## ## 140 | ## FUNCTIONS ## 141 | ## ## 142 | ############################# 143 | 144 | 145 | 146 | 147 | 148 | ############################################################################################### 149 | ## Function: initialize () 150 | ## 151 | ## Does all the script startup jibberish. 152 | ## 153 | ############################################################################################### 154 | sub initialize { 155 | 156 | ## Set STDOUT to flush immediatly after each print 157 | $| = 1; 158 | 159 | ## Intercept signals 160 | $SIG{'QUIT'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; 161 | $SIG{'INT'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; 162 | $SIG{'KILL'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; 163 | $SIG{'TERM'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; 164 | 165 | ## ALARM and HUP signals are not supported in Win32 166 | unless ($^O =~ /win/i) { 167 | $SIG{'HUP'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; 168 | $SIG{'ALRM'} = sub { quit("EXITING: Received SIG$_[0]", 1); }; 169 | } 170 | 171 | ## Fixup $conf{'programName'} 172 | $conf{'programName'} =~ s/(.)*[\/,\\]//; 173 | $0 = $conf{'programName'} . " " . join(" ", @ARGV); 174 | 175 | ## Fixup $conf{'hostname'} and $opt{'fqdn'} 176 | if ($opt{'fqdn'} eq 'changeme') { $opt{'fqdn'} = get_hostname(1); } 177 | if ($conf{'hostname'} eq 'changeme') { $conf{'hostname'} = $opt{'fqdn'}; $conf{'hostname'} =~ s/\..*//; } 178 | 179 | return(1); 180 | } 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | ############################################################################################### 197 | ## Function: processCommandLine () 198 | ## 199 | ## Processes command line storing important data in global vars (usually %conf) 200 | ## 201 | ############################################################################################### 202 | sub processCommandLine { 203 | 204 | 205 | ############################ 206 | ## Process command line ## 207 | ############################ 208 | 209 | my @ARGS = @ARGV; ## This is so later we can re-parse the command line args later if we need to 210 | my $numargv = @ARGS; 211 | help() unless ($numargv); 212 | my $counter = 0; 213 | 214 | for ($counter = 0; $counter < $numargv; $counter++) { 215 | 216 | if ($ARGS[$counter] =~ /^-h$/i) { ## Help ## 217 | help(); 218 | } 219 | 220 | elsif ($ARGS[$counter] eq "") { ## Ignore null arguments 221 | ## Do nothing 222 | } 223 | 224 | elsif ($ARGS[$counter] =~ /^--help/) { ## Topical Help ## 225 | $counter++; 226 | if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { 227 | helpTopic($ARGS[$counter]); 228 | } 229 | else { 230 | help(); 231 | } 232 | } 233 | 234 | elsif ($ARGS[$counter] =~ /^-o$/i) { ## Options specified with -o ## 235 | $counter++; 236 | ## Loop through each option passed after the -o 237 | while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { 238 | 239 | if ($ARGS[$counter] !~ /(\S+)=(\S.*)/) { 240 | printmsg("WARNING => Name/Value pair [$ARGS[$counter]] is not properly formatted", 0); 241 | printmsg("WARNING => Arguments proceeding -o should be in the form of \"name=value\"", 0); 242 | } 243 | else { 244 | if (exists($opt{$1})) { 245 | if ($1 eq 'message-header') { 246 | $opt{$1} .= $2 . $CRLF; 247 | } 248 | else { 249 | $opt{$1} = $2; 250 | } 251 | printmsg("DEBUG => Assigned \$opt{} key/value: $1 => $2", 3); 252 | } 253 | else { 254 | printmsg("WARNING => Name/Value pair [$ARGS[$counter]] will be ignored: unknown key [$1]", 0); 255 | printmsg("HINT => Try the --help option to find valid command line arguments", 1); 256 | } 257 | } 258 | $counter++; 259 | } $counter--; 260 | } 261 | 262 | elsif ($ARGS[$counter] =~ /^-f$/) { ## From ## 263 | $counter++; 264 | if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $from = $ARGS[$counter]; } 265 | else { printmsg("WARNING => The argument after -f was not an email address!", 0); $counter--; } 266 | } 267 | 268 | elsif ($ARGS[$counter] =~ /^-t$/) { ## To ## 269 | $counter++; 270 | while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { 271 | if ($ARGS[$counter] =~ /[;,]/) { 272 | push (@to, split(/[;,]/, $ARGS[$counter])); 273 | } 274 | else { 275 | push (@to,$ARGS[$counter]); 276 | } 277 | $counter++; 278 | } $counter--; 279 | } 280 | 281 | elsif ($ARGS[$counter] =~ /^-cc$/) { ## Cc ## 282 | $counter++; 283 | while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { 284 | if ($ARGS[$counter] =~ /[;,]/) { 285 | push (@cc, split(/[;,]/, $ARGS[$counter])); 286 | } 287 | else { 288 | push (@cc,$ARGS[$counter]); 289 | } 290 | $counter++; 291 | } $counter--; 292 | } 293 | 294 | elsif ($ARGS[$counter] =~ /^-bcc$/) { ## Bcc ## 295 | $counter++; 296 | while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { 297 | if ($ARGS[$counter] =~ /[;,]/) { 298 | push (@bcc, split(/[;,]/, $ARGS[$counter])); 299 | } 300 | else { 301 | push (@bcc,$ARGS[$counter]); 302 | } 303 | $counter++; 304 | } $counter--; 305 | } 306 | 307 | elsif ($ARGS[$counter] =~ /^-m$/) { ## Message ## 308 | $counter++; 309 | $message = ""; 310 | while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { 311 | if ($message) { $message .= " "; } 312 | $message .= $ARGS[$counter]; 313 | $counter++; 314 | } $counter--; 315 | 316 | ## Replace '\n' with $CRLF. 317 | ## This allows newlines with messages sent on the command line 318 | $message =~ s/\\n/$CRLF/g; 319 | } 320 | 321 | elsif ($ARGS[$counter] =~ /^-u$/) { ## Subject ## 322 | $counter++; 323 | $subject = ""; 324 | while ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { 325 | if ($subject) { $subject .= " "; } 326 | $subject .= $ARGS[$counter]; 327 | $counter++; 328 | } $counter--; 329 | } 330 | 331 | elsif ($ARGS[$counter] =~ /^-s$/) { ## Server ## 332 | $counter++; 333 | if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { 334 | $conf{'server'} = $ARGS[$counter]; 335 | if ($conf{'server'} =~ /:/) { ## Port ## 336 | ($conf{'server'},$conf{'port'}) = split(":",$conf{'server'}); 337 | } 338 | } 339 | else { printmsg("WARNING - The argument after -s was not the server!", 0); $counter--; } 340 | } 341 | 342 | elsif ($ARGS[$counter] =~ /^-b$/) { ## Bind Address ## 343 | $counter++; 344 | if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { 345 | $conf{'bindaddr'} = $ARGS[$counter]; 346 | } 347 | else { printmsg("WARNING - The argument after -b was not the bindaddr!", 0); $counter--; } 348 | } 349 | 350 | elsif ($ARGS[$counter] =~ /^-a$/) { ## Attachments ## 351 | $counter++; 352 | while ($ARGS[$counter] && ($ARGS[$counter] !~ /^-/)) { 353 | push (@attachments,$ARGS[$counter]); 354 | $counter++; 355 | } $counter--; 356 | } 357 | 358 | elsif ($ARGS[$counter] =~ /^-xu$/) { ## AuthSMTP Username ## 359 | $counter++; 360 | if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { 361 | $opt{'username'} = $ARGS[$counter]; 362 | } 363 | else { 364 | printmsg("WARNING => The argument after -xu was not valid username!", 0); 365 | $counter--; 366 | } 367 | } 368 | 369 | elsif ($ARGS[$counter] =~ /^-xp$/) { ## AuthSMTP Password ## 370 | $counter++; 371 | if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { 372 | $opt{'password'} = $ARGS[$counter]; 373 | } 374 | else { 375 | printmsg("WARNING => The argument after -xp was not valid password!", 0); 376 | $counter--; 377 | } 378 | } 379 | 380 | elsif ($ARGS[$counter] =~ /^-l$/) { ## Logging ## 381 | $counter++; 382 | $conf{'logging'} = 1; 383 | if ($ARGS[$counter] && $ARGS[$counter] !~ /^-/) { $conf{'logFile'} = $ARGS[$counter]; } 384 | else { printmsg("WARNING - The argument after -l was not the log file!", 0); $counter--; } 385 | } 386 | 387 | elsif ($ARGS[$counter] =~ s/^-v+//i) { ## Verbosity ## 388 | my $tmp = (length($&) - 1); 389 | $conf{'debug'} += $tmp; 390 | } 391 | 392 | elsif ($ARGS[$counter] =~ /^-q$/) { ## Quiet ## 393 | $conf{'stdout'} = 0; 394 | } 395 | 396 | else { 397 | printmsg("Error: \"$ARGS[$counter]\" is not a recognized option!", 0); 398 | help(); 399 | } 400 | 401 | } 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | ################################################### 411 | ## Verify required variables are set correctly ## 412 | ################################################### 413 | 414 | ## Make sure we have something in $conf{hostname} and $opt{fqdn} 415 | if ($opt{'fqdn'} =~ /\./) { 416 | $conf{'hostname'} = $opt{'fqdn'}; 417 | $conf{'hostname'} =~ s/\..*//; 418 | } 419 | 420 | if (!$conf{'server'}) { $conf{'server'} = 'localhost'; } 421 | if (!$conf{'port'}) { $conf{'port'} = 25; } 422 | if (!$from) { 423 | quit("ERROR => You must specify a 'from' field! Try --help.", 1); 424 | } 425 | if ( ((scalar(@to)) + (scalar(@cc)) + (scalar(@bcc))) <= 0) { 426 | quit("ERROR => You must specify at least one recipient via -t, -cc, or -bcc", 1); 427 | } 428 | 429 | ## Make sure email addresses look OK. 430 | foreach my $addr (@to, @cc, @bcc, $from, $opt{'reply-to'}) { 431 | if ($addr) { 432 | if (!returnAddressParts($addr)) { 433 | printmsg("ERROR => Can't use improperly formatted email address: $addr", 0); 434 | printmsg("HINT => Try viewing the extended help on addressing with \"--help addressing\"", 1); 435 | quit("", 1); 436 | } 437 | } 438 | } 439 | 440 | ## Make sure all attachments exist. 441 | foreach my $file (@attachments) { 442 | if ( (! -f $file) or (! -r $file) ) { 443 | printmsg("ERROR => The attachment [$file] doesn't exist!", 0); 444 | printmsg("HINT => Try specifying the full path to the file or reading extended help with \"--help message\"", 1); 445 | quit("", 1); 446 | } 447 | } 448 | 449 | if ($conf{'logging'} and (!$conf{'logFile'})) { 450 | quit("ERROR => You used -l to enable logging but didn't specify a log file!", 1); 451 | } 452 | 453 | if ( $opt{'username'} ) { 454 | if (!$opt{'password'}) { 455 | ## Prompt for a password since one wasn't specified with the -xp option. 456 | $SIG{'ALRM'} = sub { quit("ERROR => Timeout waiting for password inpupt", 1); }; 457 | alarm(60) if ($^O !~ /win/i); ## alarm() doesn't work in win32 458 | print "Password: "; 459 | $opt{'password'} = ; chomp $opt{'password'}; 460 | if (!$opt{'password'}) { 461 | quit("ERROR => A username for SMTP authentication was specified, but no password!", 1); 462 | } 463 | } 464 | } 465 | 466 | ## Validate the TLS setting 467 | $opt{'tls'} = lc($opt{'tls'}); 468 | if ($opt{'tls'} !~ /^(auto|yes|no)$/) { 469 | quit("ERROR => Invalid TLS setting ($opt{'tls'}). Must be one of auto, yes, or no.", 1); 470 | } 471 | 472 | ## If TLS is set to "yes", make sure sendEmail loaded the libraries needed. 473 | if ($opt{'tls'} eq 'yes' and $conf{'tls_client'} == 0) { 474 | quit("ERROR => No TLS support! SendEmail can't load required libraries. (try installing Net::SSLeay and IO::Socket::SSL)", 1); 475 | } 476 | 477 | ## Return 0 errors 478 | return(0); 479 | } 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | ## getline($socketRef) 497 | sub getline { 498 | my ($socketRef) = @_; 499 | local ($/) = "\r\n"; 500 | return $$socketRef->getline; 501 | } 502 | 503 | 504 | 505 | 506 | ## Receive a (multiline?) SMTP response from ($socketRef) 507 | sub getResponse { 508 | my ($socketRef) = @_; 509 | my ($tmp, $reply); 510 | local ($/) = "\r\n"; 511 | return undef unless defined($tmp = getline($socketRef)); 512 | return("getResponse() socket is not open") unless ($$socketRef->opened); 513 | ## Keep reading lines if it's a multi-line response 514 | while ($tmp =~ /^\d{3}-/o) { 515 | $reply .= $tmp; 516 | return undef unless defined($tmp = getline($socketRef)); 517 | } 518 | $reply .= $tmp; 519 | $reply =~ s/\r?\n$//o; 520 | return $reply; 521 | } 522 | 523 | 524 | 525 | 526 | ############################################################################################### 527 | ## Function: SMTPchat ( [string $command] ) 528 | ## 529 | ## Description: Sends $command to the SMTP server (on SERVER) and awaits a successful 530 | ## reply form the server. If the server returns an error, or does not reply 531 | ## within $conf{'alarm'} seconds an error is generated. 532 | ## NOTE: $command is optional, if no command is specified then nothing will 533 | ## be sent to the server, but a valid response is still required from the server. 534 | ## 535 | ## Input: [$command] A (optional) valid SMTP command (ex. "HELO") 536 | ## 537 | ## 538 | ## Output: Returns zero on success, or non-zero on error. 539 | ## Error messages will be stored in $conf{'error'} 540 | ## A copy of the last SMTP response is stored in the global variable 541 | ## $conf{'SMTPchat_response'} 542 | ## 543 | ## 544 | ## Example: SMTPchat ("HELO mail.isp.net"); 545 | ############################################################################################### 546 | sub SMTPchat { 547 | my ($command) = @_; 548 | 549 | printmsg("INFO => Sending: \t$command", 1) if ($command); 550 | 551 | ## Send our command 552 | print $SERVER "$command$CRLF" if ($command); 553 | 554 | ## Read a response from the server 555 | $SIG{'ALRM'} = sub { $conf{'error'} = "alarm"; $SERVER->close(); }; 556 | alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32; 557 | my $result = $conf{'SMTPchat_response'} = getResponse(\$SERVER); 558 | alarm(0) if ($^O !~ /win/i); ## alarm() doesn't work in win32; 559 | 560 | ## Generate an alert if we timed out 561 | if ($conf{'error'} eq "alarm") { 562 | $conf{'error'} = "ERROR => Timeout while reading from $conf{'server'}:$conf{'port'} There was no response after $conf{'alarm'} seconds."; 563 | return(1); 564 | } 565 | 566 | ## Make sure the server actually responded 567 | if (!$result) { 568 | $conf{'error'} = "ERROR => $conf{'server'}:$conf{'port'} returned a zero byte response to our query."; 569 | return(2); 570 | } 571 | 572 | ## Validate the response 573 | if (evalSMTPresponse($result)) { 574 | ## conf{'error'} will already be set here 575 | return(2); 576 | } 577 | 578 | ## Print the success messsage 579 | printmsg($conf{'error'}, 1); 580 | 581 | ## Return Success 582 | return(0); 583 | } 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | ############################################################################################### 597 | ## Function: evalSMTPresponse (string $message ) 598 | ## 599 | ## Description: Searches $message for either an SMTP success or error code, and returns 600 | ## 0 on success, and the actual error code on error. 601 | ## 602 | ## 603 | ## Input: $message Data received from a SMTP server (ex. "220 604 | ## 605 | ## 606 | ## Output: Returns zero on success, or non-zero on error. 607 | ## Error messages will be stored in $conf{'error'} 608 | ## 609 | ## 610 | ## Example: SMTPchat ("HELO mail.isp.net"); 611 | ############################################################################################### 612 | sub evalSMTPresponse { 613 | my ($message) = @_; 614 | 615 | ## Validate input 616 | if (!$message) { 617 | $conf{'error'} = "ERROR => No message was passed to evalSMTPresponse(). What happened?"; 618 | return(1) 619 | } 620 | 621 | printmsg("DEBUG => evalSMTPresponse() - Checking for SMTP success or error status in the message: $message ", 3); 622 | 623 | ## Look for a SMTP success code 624 | if ($message =~ /^([23]\d\d)/) { 625 | printmsg("DEBUG => evalSMTPresponse() - Found SMTP success code: $1", 2); 626 | $conf{'error'} = "SUCCESS => Received: \t$message"; 627 | return(0); 628 | } 629 | 630 | ## Look for a SMTP error code 631 | if ($message =~ /^([45]\d\d)/) { 632 | printmsg("DEBUG => evalSMTPresponse() - Found SMTP error code: $1", 2); 633 | $conf{'error'} = "ERROR => Received: \t$message"; 634 | return($1); 635 | } 636 | 637 | ## If no SMTP codes were found return an error of 1 638 | $conf{'error'} = "ERROR => Received a message with no success or error code. The message received was: $message"; 639 | return(2); 640 | 641 | } 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | ######################################################### 653 | # SUB: &return_month(0,1,etc) 654 | # returns the name of the month that corrosponds 655 | # with the number. returns 0 on error. 656 | ######################################################### 657 | sub return_month { 658 | my $x = $_[0]; 659 | if ($x == 0) { return 'Jan'; } 660 | if ($x == 1) { return 'Feb'; } 661 | if ($x == 2) { return 'Mar'; } 662 | if ($x == 3) { return 'Apr'; } 663 | if ($x == 4) { return 'May'; } 664 | if ($x == 5) { return 'Jun'; } 665 | if ($x == 6) { return 'Jul'; } 666 | if ($x == 7) { return 'Aug'; } 667 | if ($x == 8) { return 'Sep'; } 668 | if ($x == 9) { return 'Oct'; } 669 | if ($x == 10) { return 'Nov'; } 670 | if ($x == 11) { return 'Dec'; } 671 | return (0); 672 | } 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | ######################################################### 690 | # SUB: &return_day(0,1,etc) 691 | # returns the name of the day that corrosponds 692 | # with the number. returns 0 on error. 693 | ######################################################### 694 | sub return_day { 695 | my $x = $_[0]; 696 | if ($x == 0) { return 'Sun'; } 697 | if ($x == 1) { return 'Mon'; } 698 | if ($x == 2) { return 'Tue'; } 699 | if ($x == 3) { return 'Wed'; } 700 | if ($x == 4) { return 'Thu'; } 701 | if ($x == 5) { return 'Fri'; } 702 | if ($x == 6) { return 'Sat'; } 703 | return (0); 704 | } 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | ############################################################################################### 722 | ## Function: returnAddressParts(string $address) 723 | ## 724 | ## Description: Returns a two element array containing the "Name" and "Address" parts of 725 | ## an email address. 726 | ## 727 | ## Example: "Brandon Zehm " 728 | ## would return: ("Brandon Zehm", "caspian@dotconf.net"); 729 | ## 730 | ## "caspian@dotconf.net" 731 | ## would return: ("caspian@dotconf.net", "caspian@dotconf.net") 732 | ############################################################################################### 733 | sub returnAddressParts { 734 | my $input = $_[0]; 735 | my $name = ""; 736 | my $address = ""; 737 | 738 | ## Make sure to fail if it looks totally invalid 739 | if ($input !~ /(\S+\@\S+)/) { 740 | $conf{'error'} = "ERROR => The address [$input] doesn't look like a valid email address, ignoring it"; 741 | return(undef()); 742 | } 743 | 744 | ## Check 1, should find addresses like: "Brandon Zehm " 745 | elsif ($input =~ /^\s*(\S(.*\S)?)\s*<(\S+\@\S+)>/o) { 746 | ($name, $address) = ($1, $3); 747 | } 748 | 749 | ## Otherwise if that failed, just get the address: 750 | elsif ($input =~ /<(\S+\@\S+)>/o) { 751 | $name = $address = $1; 752 | } 753 | 754 | ## Or maybe it was formatted this way: caspian@dotconf.net 755 | elsif ($input =~ /(\S+\@\S+)/o) { 756 | $name = $address = $1; 757 | } 758 | 759 | ## Something stupid happened, just return an error. 760 | unless ($name and $address) { 761 | printmsg("ERROR => Couldn't parse the address: $input", 0); 762 | printmsg("HINT => If you think this should work, consider reporting this as a bug to $conf{'authorEmail'}", 1); 763 | return(undef()); 764 | } 765 | 766 | ## Make sure there aren't invalid characters in the address, and return it. 767 | my $ctrl = '\000-\037'; 768 | my $nonASCII = '\x80-\xff'; 769 | if ($address =~ /[<> ,;:"'\[\]\\$ctrl$nonASCII]/) { 770 | printmsg("WARNING => The address [$address] seems to contain invalid characters: continuing anyway", 0); 771 | } 772 | return($name, $address); 773 | } 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | ############################################################################################### 791 | ## Function: base64_encode(string $data, bool $chunk) 792 | ## 793 | ## Description: Returns $data as a base64 encoded string. 794 | ## If $chunk is true, the encoded data is returned in 76 character long lines 795 | ## with the final \CR\LF removed. 796 | ## 797 | ## Note: This is only used from the smtp auth section of code. 798 | ## At some point it would be nice to merge the code that encodes attachments and this. 799 | ############################################################################################### 800 | sub base64_encode { 801 | my $data = $_[0]; 802 | my $chunk = $_[1]; 803 | my $tmp = ''; 804 | my $base64 = ''; 805 | my $CRLF = "\r\n"; 806 | 807 | ################################### 808 | ## Convert binary data to base64 ## 809 | ################################### 810 | while ($data =~ s/(.{45})//s) { ## Get 45 bytes from the binary string 811 | $tmp = substr(pack('u', $&), 1); ## Convert the binary to uuencoded text 812 | chop($tmp); 813 | $tmp =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 814 | $base64 .= $tmp; 815 | } 816 | 817 | ########################## 818 | ## Encode the leftovers ## 819 | ########################## 820 | my $padding = ""; 821 | if ( ($data) and (length($data) > 0) ) { 822 | $padding = (3 - length($data) % 3) % 3; ## Set flag if binary data isn't divisible by 3 823 | $tmp = substr(pack('u', $data), 1); ## Convert the binary to uuencoded text 824 | chop($tmp); 825 | $tmp =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 826 | $base64 .= $tmp; 827 | } 828 | 829 | ############################ 830 | ## Fix padding at the end ## 831 | ############################ 832 | $data = ''; 833 | $base64 =~ s/.{$padding}$/'=' x $padding/e if $padding; ## Fix the end padding if flag (from above) is set 834 | if ($chunk) { 835 | while ($base64 =~ s/(.{1,76})//s) { ## Put $CRLF after each 76 characters 836 | $data .= "$1$CRLF"; 837 | } 838 | } 839 | else { 840 | $data = $base64; 841 | } 842 | 843 | ## Remove any trailing CRLF's 844 | $data =~ s/(\r|\n)*$//s; 845 | return($data); 846 | } 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | ######################################################### 857 | # SUB: send_attachment("/path/filename") 858 | # Sends the mime headers and base64 encoded file 859 | # to the email server. 860 | ######################################################### 861 | sub send_attachment { 862 | my ($filename) = @_; ## Get filename passed 863 | my (@fields, $y, $filename_name, $encoding, ## Local variables 864 | @attachlines, $content_type); 865 | my $bin = 1; 866 | 867 | @fields = split(/\/|\\/, $filename); ## Get the actual filename without the path 868 | $filename_name = pop(@fields); 869 | push @attachments_names, $filename_name; ## FIXME: This is only used later for putting in the log file 870 | 871 | ########################## 872 | ## Autodetect Mime Type ## 873 | ########################## 874 | 875 | @fields = split(/\./, $filename_name); 876 | $encoding = $fields[$#fields]; 877 | 878 | if ($encoding =~ /txt|text|log|conf|^c$|cpp|^h$|inc|m3u/i) { $content_type = 'text/plain'; } 879 | elsif ($encoding =~ /html|htm|shtml|shtm|asp|php|cfm/i) { $content_type = 'text/html'; } 880 | elsif ($encoding =~ /sh$/i) { $content_type = 'application/x-sh'; } 881 | elsif ($encoding =~ /tcl/i) { $content_type = 'application/x-tcl'; } 882 | elsif ($encoding =~ /pl$/i) { $content_type = 'application/x-perl'; } 883 | elsif ($encoding =~ /js$/i) { $content_type = 'application/x-javascript'; } 884 | elsif ($encoding =~ /man/i) { $content_type = 'application/x-troff-man'; } 885 | elsif ($encoding =~ /gif/i) { $content_type = 'image/gif'; } 886 | elsif ($encoding =~ /jpg|jpeg|jpe|jfif|pjpeg|pjp/i) { $content_type = 'image/jpeg'; } 887 | elsif ($encoding =~ /tif|tiff/i) { $content_type = 'image/tiff'; } 888 | elsif ($encoding =~ /xpm/i) { $content_type = 'image/x-xpixmap'; } 889 | elsif ($encoding =~ /bmp/i) { $content_type = 'image/x-MS-bmp'; } 890 | elsif ($encoding =~ /pcd/i) { $content_type = 'image/x-photo-cd'; } 891 | elsif ($encoding =~ /png/i) { $content_type = 'image/png'; } 892 | elsif ($encoding =~ /aif|aiff/i) { $content_type = 'audio/x-aiff'; } 893 | elsif ($encoding =~ /wav/i) { $content_type = 'audio/x-wav'; } 894 | elsif ($encoding =~ /mp2|mp3|mpa/i) { $content_type = 'audio/x-mpeg'; } 895 | elsif ($encoding =~ /ra$|ram/i) { $content_type = 'audio/x-pn-realaudio'; } 896 | elsif ($encoding =~ /mpeg|mpg/i) { $content_type = 'video/mpeg'; } 897 | elsif ($encoding =~ /mov|qt$/i) { $content_type = 'video/quicktime'; } 898 | elsif ($encoding =~ /avi/i) { $content_type = 'video/x-msvideo'; } 899 | elsif ($encoding =~ /zip/i) { $content_type = 'application/x-zip-compressed'; } 900 | elsif ($encoding =~ /tar/i) { $content_type = 'application/x-tar'; } 901 | elsif ($encoding =~ /jar/i) { $content_type = 'application/java-archive'; } 902 | elsif ($encoding =~ /exe|bin/i) { $content_type = 'application/octet-stream'; } 903 | elsif ($encoding =~ /ppt|pot|ppa|pps|pwz/i) { $content_type = 'application/vnd.ms-powerpoint'; } 904 | elsif ($encoding =~ /mdb|mda|mde/i) { $content_type = 'application/vnd.ms-access'; } 905 | elsif ($encoding =~ /xls|xlt|xlm|xld|xla|xlc|xlw|xll/i) { $content_type = 'application/vnd.ms-excel'; } 906 | elsif ($encoding =~ /doc|dot/i) { $content_type = 'application/msword'; } 907 | elsif ($encoding =~ /rtf/i) { $content_type = 'application/rtf'; } 908 | elsif ($encoding =~ /pdf/i) { $content_type = 'application/pdf'; } 909 | elsif ($encoding =~ /tex/i) { $content_type = 'application/x-tex'; } 910 | elsif ($encoding =~ /latex/i) { $content_type = 'application/x-latex'; } 911 | elsif ($encoding =~ /vcf/i) { $content_type = 'application/x-vcard'; } 912 | else { $content_type = 'application/octet-stream'; } 913 | 914 | 915 | ############################ 916 | ## Process the attachment ## 917 | ############################ 918 | 919 | ##################################### 920 | ## Generate and print MIME headers ## 921 | ##################################### 922 | 923 | $y = "$CRLF--$conf{'delimiter'}$CRLF"; 924 | $y .= "Content-Type: $content_type;$CRLF"; 925 | $y .= " name=\"$filename_name\"$CRLF"; 926 | $y .= "Content-Transfer-Encoding: base64$CRLF"; 927 | $y .= "Content-Disposition: attachment; filename=\"$filename_name\"$CRLF"; 928 | $y .= "$CRLF"; 929 | print $SERVER $y; 930 | 931 | 932 | ########################################################### 933 | ## Convert the file to base64 and print it to the server ## 934 | ########################################################### 935 | 936 | open (FILETOATTACH, $filename) || do { 937 | printmsg("ERROR => Opening the file [$filename] for attachment failed with the error: $!", 0); 938 | return(1); 939 | }; 940 | binmode(FILETOATTACH); ## Hack to make Win32 work 941 | 942 | my $res = ""; 943 | my $tmp = ""; 944 | my $base64 = ""; 945 | while () { ## Read a line from the (binary) file 946 | $res .= $_; 947 | 948 | ################################### 949 | ## Convert binary data to base64 ## 950 | ################################### 951 | while ($res =~ s/(.{45})//s) { ## Get 45 bytes from the binary string 952 | $tmp = substr(pack('u', $&), 1); ## Convert the binary to uuencoded text 953 | chop($tmp); 954 | $tmp =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 955 | $base64 .= $tmp; 956 | } 957 | 958 | ################################ 959 | ## Print chunks to the server ## 960 | ################################ 961 | while ($base64 =~ s/(.{76})//s) { 962 | print $SERVER "$1$CRLF"; 963 | } 964 | 965 | } 966 | 967 | ################################### 968 | ## Encode and send the leftovers ## 969 | ################################### 970 | my $padding = ""; 971 | if ( ($res) and (length($res) >= 1) ) { 972 | $padding = (3 - length($res) % 3) % 3; ## Set flag if binary data isn't divisible by 3 973 | $res = substr(pack('u', $res), 1); ## Convert the binary to uuencoded text 974 | chop($res); 975 | $res =~ tr|` -_|AA-Za-z0-9+/|; ## Translate from uuencode to base64 976 | } 977 | 978 | ############################ 979 | ## Fix padding at the end ## 980 | ############################ 981 | $res = $base64 . $res; ## Get left overs from above 982 | $res =~ s/.{$padding}$/'=' x $padding/e if $padding; ## Fix the end padding if flag (from above) is set 983 | if ($res) { 984 | while ($res =~ s/(.{1,76})//s) { ## Send it to the email server. 985 | print $SERVER "$1$CRLF"; 986 | } 987 | } 988 | 989 | close (FILETOATTACH) || do { 990 | printmsg("ERROR - Closing the filehandle for file [$filename] failed with the error: $!", 0); 991 | return(2); 992 | }; 993 | 994 | ## Return 0 errors 995 | return(0); 996 | 997 | } 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | ############################################################################################### 1008 | ## Function: $string = get_hostname (boot $fqdn) 1009 | ## 1010 | ## Description: Tries really hard to returns the short (or FQDN) hostname of the current 1011 | ## system. Uses techniques and code from the Sys-Hostname module. 1012 | ## 1013 | ## Input: $fqdn A true value (1) will cause this function to return a FQDN hostname 1014 | ## rather than a short hostname. 1015 | ## 1016 | ## Output: Returns a string 1017 | ############################################################################################### 1018 | sub get_hostname { 1019 | ## Assign incoming parameters to variables 1020 | my ( $fqdn ) = @_; 1021 | my $hostname = ""; 1022 | 1023 | ## STEP 1: Get short hostname 1024 | 1025 | ## Load Sys::Hostname if it's available 1026 | eval { require Sys::Hostname; }; 1027 | unless ($@) { 1028 | $hostname = Sys::Hostname::hostname(); 1029 | } 1030 | 1031 | ## If that didn't get us a hostname, try a few other things 1032 | else { 1033 | ## Windows systems 1034 | if ($^O !~ /win/i) { 1035 | if ($ENV{'COMPUTERNAME'}) { $hostname = $ENV{'COMPUTERNAME'}; } 1036 | if (!$hostname) { $hostname = gethostbyname('localhost'); } 1037 | if (!$hostname) { chomp($hostname = `hostname 2> NUL`) }; 1038 | } 1039 | 1040 | ## Unix systems 1041 | else { 1042 | local $ENV{PATH} = '/usr/bin:/bin:/usr/sbin:/sbin'; ## Paranoia 1043 | 1044 | ## Try the environment first (Help! What other variables could/should I be checking here?) 1045 | if ($ENV{'HOSTNAME'}) { $hostname = $ENV{'HOSTNAME'}; } 1046 | 1047 | ## Try the hostname command 1048 | eval { local $SIG{__DIE__}; local $SIG{CHLD}; $hostname = `hostname 2>/dev/null`; chomp($hostname); } || 1049 | 1050 | ## Try POSIX::uname(), which strictly can't be expected to be correct 1051 | eval { local $SIG{__DIE__}; require POSIX; $hostname = (POSIX::uname())[1]; } || 1052 | 1053 | ## Try the uname command 1054 | eval { local $SIG{__DIE__}; $hostname = `uname -n 2>/dev/null`; chomp($hostname); }; 1055 | 1056 | } 1057 | 1058 | ## If we can't find anything else, return "" 1059 | if (!$hostname) { 1060 | print "WARNING => No hostname could be determined, please specify one with -o fqdn=FQDN option!\n"; 1061 | return("unknown"); 1062 | } 1063 | } 1064 | 1065 | ## Return the short hostname 1066 | unless ($fqdn) { 1067 | $hostname =~ s/\..*//; 1068 | return(lc($hostname)); 1069 | } 1070 | 1071 | ## STEP 2: Determine the FQDN 1072 | 1073 | ## First, if we already have one return it. 1074 | if ($hostname =~ /\w\.\w/) { return(lc($hostname)); } 1075 | 1076 | ## Next try using 1077 | eval { $fqdn = (gethostbyname($hostname))[0]; }; 1078 | if ($fqdn) { return(lc($fqdn)); } 1079 | return(lc($hostname)); 1080 | } 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | ############################################################################################### 1090 | ## Function: printmsg (string $message, int $level) 1091 | ## 1092 | ## Description: Handles all messages - printing them to the screen only if the messages 1093 | ## $level is >= the global debug level. If $conf{'logFile'} is defined it 1094 | ## will also log the message to that file. 1095 | ## 1096 | ## Input: $message A message to be printed, logged, etc. 1097 | ## $level The debug level of the message. If 1098 | ## not defined 0 will be assumed. 0 is 1099 | ## considered a normal message, 1 and 1100 | ## higher is considered a debug message. 1101 | ## 1102 | ## Output: Prints to STDOUT 1103 | ## 1104 | ## Assumptions: $conf{'hostname'} should be the name of the computer we're running on. 1105 | ## $conf{'stdout'} should be set to 1 if you want to print to stdout 1106 | ## $conf{'logFile'} should be a full path to a log file if you want that 1107 | ## $conf{'debug'} should be an integer between 0 and 10. 1108 | ## 1109 | ## Example: printmsg("WARNING: We believe in generic error messages... NOT!", 0); 1110 | ############################################################################################### 1111 | sub printmsg { 1112 | ## Assign incoming parameters to variables 1113 | my ( $message, $level ) = @_; 1114 | 1115 | ## Make sure input is sane 1116 | $level = 0 if (!defined($level)); 1117 | $message =~ s/\s+$//sgo; 1118 | $message =~ s/\r?\n/, /sgo; 1119 | 1120 | ## Continue only if the debug level of the program is >= message debug level. 1121 | if ($conf{'debug'} >= $level) { 1122 | 1123 | ## Get the date in the format: Dec 3 11:14:04 1124 | my ($sec, $min, $hour, $mday, $mon) = localtime(); 1125 | $mon = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')[$mon]; 1126 | my $date = sprintf("%s %02d %02d:%02d:%02d", $mon, $mday, $hour, $min, $sec); 1127 | 1128 | ## Print to STDOUT always if debugging is enabled, or if conf{stdout} is true. 1129 | if ( ($conf{'debug'} >= 1) or ($conf{'stdout'} == 1) ) { 1130 | print "$date $conf{'hostname'} $conf{'programName'}\[$$\]: $message\n"; 1131 | } 1132 | 1133 | ## Print to the log file if $conf{'logging'} is true 1134 | if ($conf{'logFile'}) { 1135 | if (openLogFile($conf{'logFile'})) { $conf{'logFile'} = ""; printmsg("ERROR => Opening the file [$conf{'logFile'}] for appending returned the error: $!", 1); } 1136 | print LOGFILE "$date $conf{'hostname'} $conf{'programName'}\[$$\]: $message\n"; 1137 | } 1138 | 1139 | } 1140 | 1141 | ## Return 0 errors 1142 | return(0); 1143 | } 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1156 | ############################################################################################### 1157 | ## FUNCTION: 1158 | ## openLogFile ( $filename ) 1159 | ## 1160 | ## 1161 | ## DESCRIPTION: 1162 | ## Opens the file $filename and attaches it to the filehandle "LOGFILE". Returns 0 on success 1163 | ## and non-zero on failure. Error codes are listed below, and the error message gets set in 1164 | ## global variable $!. 1165 | ## 1166 | ## 1167 | ## Example: 1168 | ## openFile ("/var/log/sendEmail.log"); 1169 | ## 1170 | ############################################################################################### 1171 | sub openLogFile { 1172 | ## Get the incoming filename 1173 | my $filename = $_[0]; 1174 | 1175 | ## Make sure our file exists, and if the file doesn't exist then create it 1176 | if ( ! -f $filename ) { 1177 | print STDERR "NOTICE: The log file [$filename] does not exist. Creating it now with mode [0600].\n" if ($conf{'stdout'}); 1178 | open (LOGFILE, ">>$filename"); 1179 | close LOGFILE; 1180 | chmod (0600, $filename); 1181 | } 1182 | 1183 | ## Now open the file and attach it to a filehandle 1184 | open (LOGFILE,">>$filename") or return (1); 1185 | 1186 | ## Put the file into non-buffering mode 1187 | select LOGFILE; 1188 | $| = 1; 1189 | select STDOUT; 1190 | 1191 | ## Return success 1192 | return(0); 1193 | } 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 1202 | ############################################################################################### 1203 | ## Function: read_file (string $filename) 1204 | ## 1205 | ## Description: Reads the contents of a file and returns a two part array: 1206 | ## ($status, $file-contents) 1207 | ## $status is 0 on success, non-zero on error. 1208 | ## 1209 | ## Example: ($status, $file) = read_file("/etc/passwd"); 1210 | ############################################################################################### 1211 | sub read_file { 1212 | my ( $filename ) = @_; 1213 | 1214 | ## If the value specified is a file, load the file's contents 1215 | if ( (-e $filename and -r $filename) ) { 1216 | my $FILE; 1217 | if(!open($FILE, ' ' . $filename)) { 1218 | return((1, "")); 1219 | } 1220 | my $file = ''; 1221 | while (<$FILE>) { 1222 | $file .= $_; 1223 | } 1224 | ## Strip an ending \r\n 1225 | $file =~ s/\r?\n$//os; 1226 | } 1227 | return((1, "")); 1228 | } 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | ############################################################################################### 1239 | ## Function: quit (string $message, int $errorLevel) 1240 | ## 1241 | ## Description: Exits the program, optionally printing $message. It 1242 | ## returns an exit error level of $errorLevel to the 1243 | ## system (0 means no errors, and is assumed if empty.) 1244 | ## 1245 | ## Example: quit("Exiting program normally", 0); 1246 | ############################################################################################### 1247 | sub quit { 1248 | my ( $message, $errorLevel ) = @_; 1249 | $errorLevel = 0 if (!defined($errorLevel)); 1250 | 1251 | ## Print exit message 1252 | if ($message) { 1253 | printmsg($message, 0); 1254 | } 1255 | 1256 | ## Exit 1257 | exit($errorLevel); 1258 | } 1259 | 1260 | 1261 | 1262 | 1263 | 1264 | 1265 | 1266 | 1267 | 1268 | 1269 | 1270 | 1271 | ############################################################################################### 1272 | ## Function: help () 1273 | ## 1274 | ## Description: For all those newbies ;) 1275 | ## Prints a help message and exits the program. 1276 | ## 1277 | ############################################################################################### 1278 | sub help { 1279 | exit(1) if (!$conf{'stdout'}); 1280 | print <${colorNoBold} 1283 | 1284 | Synopsis: $conf{'programName'} -f ADDRESS [options] 1285 | 1286 | ${colorRed}Required:${colorNormal} 1287 | -f ADDRESS from (sender) email address 1288 | * At least one recipient required via -t, -cc, or -bcc 1289 | * Message body required via -m, STDIN, or -o message-file=FILE 1290 | 1291 | ${colorGreen}Common:${colorNormal} 1292 | -t ADDRESS [ADDR ...] to email address(es) 1293 | -u SUBJECT message subject 1294 | -m MESSAGE message body 1295 | -s SERVER[:PORT] smtp mail relay, default is $conf{'server'}:$conf{'port'} 1296 | 1297 | ${colorGreen}Optional:${colorNormal} 1298 | -a FILE [FILE ...] file attachment(s) 1299 | -cc ADDRESS [ADDR ...] cc email address(es) 1300 | -bcc ADDRESS [ADDR ...] bcc email address(es) 1301 | -xu USERNAME username for SMTP authentication 1302 | -xp PASSWORD password for SMTP authentication 1303 | 1304 | ${colorGreen}Paranormal:${colorNormal} 1305 | -b BINDADDR[:PORT] local host bind address 1306 | -l LOGFILE log to the specified file 1307 | -v verbosity, use multiple times for greater effect 1308 | -q be quiet (i.e. no STDOUT output) 1309 | -o NAME=VALUE advanced options, for details try: --help misc 1310 | -o message-content-type= 1311 | -o message-file=FILE -o message-format=raw 1312 | -o message-header=HEADER -o message-charset=CHARSET 1313 | -o reply-to=ADDRESS -o timeout=SECONDS 1314 | -o username=USERNAME -o password=PASSWORD 1315 | -o tls= -o fqdn=FQDN 1316 | 1317 | 1318 | ${colorGreen}Help:${colorNormal} 1319 | --help the helpful overview you're reading now 1320 | --help addressing explain addressing and related options 1321 | --help message explain message body input and related options 1322 | --help networking explain -s, -b, etc 1323 | --help output explain logging and other output options 1324 | --help misc explain -o options, TLS, SMTP auth, and more 1325 | 1326 | EOM 1327 | exit(1); 1328 | } 1329 | 1330 | 1331 | 1332 | 1333 | 1334 | 1335 | 1336 | 1337 | 1338 | ############################################################################################### 1339 | ## Function: helpTopic ($topic) 1340 | ## 1341 | ## Description: For all those newbies ;) 1342 | ## Prints a help message and exits the program. 1343 | ## 1344 | ############################################################################################### 1345 | sub helpTopic { 1346 | exit(1) if (!$conf{'stdout'}); 1347 | my ($topic) = @_; 1348 | 1349 | CASE: { 1350 | 1351 | 1352 | 1353 | 1354 | ## ADDRESSING 1355 | ($topic eq 'addressing') && do { 1356 | print <" 1392 | Just Address: "john.doe\@gmail.com" 1393 | 1394 | The "Full Name" method is useful if you want a name, rather than a plain 1395 | email address, to be displayed in the recipient's From, To, or Cc fields 1396 | when they view the message. 1397 | 1398 | 1399 | ${colorGreen}Multiple Recipients${colorNormal} 1400 | The -t, -cc, and -bcc options each accept multiple addresses. They may be 1401 | specified by separating them by either a white space, comma, or semi-colon 1402 | separated list. You may also specify the -t, -cc, and -bcc options multiple 1403 | times, each occurance will append the new recipients to the respective list. 1404 | 1405 | Examples: 1406 | (I used "-t" in these examples, but it can be "-cc" or "-bcc" as well) 1407 | 1408 | * Space separated list: 1409 | -t jane.doe\@yahoo.com "John Doe " 1410 | 1411 | * Semi-colon separated list: 1412 | -t "jane.doe\@yahoo.com; John Doe " 1413 | 1414 | * Comma separated list: 1415 | -t "jane.doe\@yahoo.com, John Doe " 1416 | 1417 | * Multiple -t, -cc, or -bcc options: 1418 | -t "jane.doe\@yahoo.com" -t "John Doe " 1419 | 1420 | 1421 | EOM 1422 | last CASE; 1423 | }; 1424 | 1425 | 1426 | 1427 | 1428 | 1429 | 1430 | ## MESSAGE 1431 | ($topic eq 'message') && do { 1432 | print < 1442 | -o message-header=EMAIL HEADER 1443 | -o message-charset=CHARSET 1444 | -o message-format=raw 1445 | 1446 | -u SUBJECT 1447 | This option allows you to specify the subject for your email message. 1448 | It is not required (anymore) that the subject be quoted, although it 1449 | is recommended. The subject will be read until an argument starting 1450 | with a hyphen (-) is found. 1451 | Examples: 1452 | -u "Contact information while on vacation" 1453 | -u New Microsoft vulnerability discovered 1454 | 1455 | -m MESSAGE 1456 | This option is one of three methods that allow you to specify the message 1457 | body for your email. The message may be specified on the command line 1458 | with this -m option, read from a file with the -o message-file=FILE 1459 | option, or read from STDIN if neither of these options are present. 1460 | 1461 | It is not required (anymore) that the message be quoted, although it is 1462 | recommended. The message will be read until an argument starting with a 1463 | hyphen (-) is found. 1464 | Examples: 1465 | -m "See you in South Beach, Hawaii. -Todd" 1466 | -m Please ensure that you upgrade your systems right away 1467 | 1468 | Multi-line message bodies may be specified with the -m option by putting 1469 | a "\\n" into the message. Example: 1470 | -m "This is line 1.\\nAnd this is line 2." 1471 | 1472 | HTML messages are supported, simply begin your message with "" and 1473 | sendEmail will properly label the mime header so MUAs properly render 1474 | the message. It is currently not possible without "-o message-format=raw" 1475 | to send a message with both text and html parts with sendEmail. 1476 | 1477 | -o message-file=FILE 1478 | This option is one of three methods that allow you to specify the message 1479 | body for your email. To use this option simply specify a text file 1480 | containing the body of your email message. Examples: 1481 | -o message-file=/root/message.txt 1482 | -o message-file="C:\\Program Files\\output.txt" 1483 | 1484 | -o message-content-type= 1485 | This option allows you to specify the content-type of the email. If your 1486 | email message is an html message but is being displayed as a text message 1487 | just add "-o message-content-type=html" to the command line to force it 1488 | to display as an html message. This actually just changes the Content-Type: 1489 | header. Advanced users will be happy to know that if you specify anything 1490 | other than the three options listed above it will use that as the vaule 1491 | for the Content-Type header. 1492 | 1493 | -o message-header=EMAIL HEADER 1494 | This option allows you to specify additional email headers to be included. 1495 | To add more than one message header simply use this option on the command 1496 | line more than once. If you specify a message header that sendEmail would 1497 | normally generate the one you specified will be used in it's place. 1498 | Do not use this unless you know what you are doing! 1499 | Example: 1500 | To scare a Microsoft Outlook user you may want to try this: 1501 | -o message-header="X-Message-Flag: Message contains illegal content" 1502 | Example: 1503 | To request a read-receipt try this: 1504 | -o message-header="Disposition-Notification-To: " 1505 | Example: 1506 | To set the message priority try this: 1507 | -o message-header="X-Priority: 1" 1508 | Priority reference: 1=highest, 2=high, 3=normal, 4=low, 5=lowest 1509 | 1510 | -o message-charset=CHARSET 1511 | This option allows you to specify the character-set for the message body. 1512 | The default is iso-8859-1. 1513 | 1514 | -o message-format=raw 1515 | This option instructs sendEmail to assume the message (specified with -m, 1516 | read from STDIN, or read from the file specified in -o message-file=FILE) 1517 | is already a *complete* email message. SendEmail will not generate any 1518 | headers and will transmit the message as-is to the remote SMTP server. 1519 | Due to the nature of this option the following command line options will 1520 | be ignored when this one is used: 1521 | -u SUBJECT 1522 | -o message-header=EMAIL HEADER 1523 | -o message-charset=CHARSET 1524 | -a ATTACHMENT 1525 | 1526 | 1527 | ${colorGreen}The Message Body${colorNormal} 1528 | The email message body may be specified in one of three ways: 1529 | 1) Via the -m MESSAGE command line option. 1530 | Example: 1531 | -m "This is the message body" 1532 | 1533 | 2) By putting the message body in a file and using the -o message-file=FILE 1534 | command line option. 1535 | Example: 1536 | -o message-file=/root/message.txt 1537 | 1538 | 3) By piping the message body to sendEmail when nither of the above command 1539 | line options were specified. 1540 | Example: 1541 | grep "ERROR" /var/log/messages | sendEmail -t you\@domain.com ... 1542 | 1543 | If the message body begins with "" then the message will be treated as 1544 | an HTML message and the MIME headers will be written so that a HTML capable 1545 | email client will display the message in it's HTML form. 1546 | Any of the above methods may be used with the -o message-format=raw option 1547 | to deliver an already complete email message. 1548 | 1549 | 1550 | EOM 1551 | last CASE; 1552 | }; 1553 | 1554 | 1555 | 1556 | 1557 | 1558 | 1559 | ## MISC 1560 | ($topic eq 'misc') && do { 1561 | print < 1573 | -o timeout=SECONDS 1574 | -o fqdn=FQDN 1575 | 1576 | -a ATTACHMENT [ATTACHMENT ...] 1577 | This option allows you to attach any number of files to your email message. 1578 | To specify more than one attachment, simply separate each filename with a 1579 | space. Example: -a file1.txt file2.txt file3.txt 1580 | 1581 | -xu USERNAME 1582 | Alias for -o username=USERNAME 1583 | 1584 | -xp PASSWORD 1585 | Alias for -o password=PASSWORD 1586 | 1587 | -o username=USERNAME (synonym for -xu) 1588 | These options allow specification of a username to be used with SMTP 1589 | servers that require authentication. If a username is specified but a 1590 | password is not, you will be prompted to enter one at runtime. 1591 | 1592 | -o password=PASSWORD (synonym for -xp) 1593 | These options allow specification of a password to be used with SMTP 1594 | servers that require authentication. If a username is specified but a 1595 | password is not, you will be prompted to enter one at runtime. 1596 | 1597 | -o tls= 1598 | This option allows you to specify if TLS (SSL for SMTP) should be enabled 1599 | or disabled. The default, auto, will use TLS automatically if your perl 1600 | installation has the IO::Socket::SSL and Net::SSLeay modules available, 1601 | and if the remote SMTP server supports TLS. To require TLS for message 1602 | delivery set this to yes. To disable TLS support set this to no. A debug 1603 | level of one or higher will reveal details about the status of TLS. 1604 | 1605 | -o timeout=SECONDS 1606 | This option sets the timeout value in seconds used for all network reads, 1607 | writes, and a few other things. 1608 | 1609 | -o fqdn=FQDN 1610 | This option sets the Fully Qualified Domain Name used during the initial 1611 | SMTP greeting. Normally this is automatically detected, but in case you 1612 | need to manually set it for some reason or get a warning about detection 1613 | failing, you can use this to override the default. 1614 | 1615 | 1616 | EOM 1617 | last CASE; 1618 | }; 1619 | 1620 | 1621 | 1622 | 1623 | 1624 | 1625 | ## NETWORKING 1626 | ($topic eq 'networking') && do { 1627 | print < 1636 | -o timeout=SECONDS 1637 | 1638 | -s SERVER[:PORT] 1639 | This option allows you to specify the SMTP server sendEmail should 1640 | connect to to deliver your email message to. If this option is not 1641 | specified sendEmail will try to connect to localhost:25 to deliver 1642 | the message. THIS IS MOST LIKELY NOT WHAT YOU WANT, AND WILL LIKELY 1643 | FAIL unless you have a email server (commonly known as an MTA) running 1644 | on your computer! 1645 | Typically you will need to specify your company or ISP's email server. 1646 | For example, if you use CableOne you will need to specify: 1647 | -s mail.cableone.net 1648 | If you have your own email server running on port 300 you would 1649 | probably use an option like this: 1650 | -s myserver.mydomain.com:300 1651 | If you're a GMail user try: 1652 | -s smtp.gmail.com:587 -xu me\@gmail.com -xp PASSWD 1653 | 1654 | -b BINDADDR[:PORT] 1655 | This option allows you to specify the local IP address (and optional 1656 | tcp port number) for sendEmail to bind to when connecting to the remote 1657 | SMTP server. This useful for people who need to send an email from a 1658 | specific network interface or source address and are running sendEmail on 1659 | a firewall or other host with several network interfaces. 1660 | 1661 | -o tls= 1662 | This option allows you to specify if TLS (SSL for SMTP) should be enabled 1663 | or disabled. The default, auto, will use TLS automatically if your perl 1664 | installation has the IO::Socket::SSL and Net::SSLeay modules available, 1665 | and if the remote SMTP server supports TLS. To require TLS for message 1666 | delivery set this to yes. To disable TLS support set this to no. A debug 1667 | level of one or higher will reveal details about the status of TLS. 1668 | 1669 | -o timeout=SECONDS 1670 | This option sets the timeout value in seconds used for all network reads, 1671 | writes, and a few other things. 1672 | 1673 | 1674 | EOM 1675 | last CASE; 1676 | }; 1677 | 1678 | 1679 | 1680 | 1681 | 1682 | 1683 | ## OUTPUT 1684 | ($topic eq 'output') && do { 1685 | print < The help topic specified is not valid!", 1); 1724 | }; 1725 | 1726 | exit(1); 1727 | } 1728 | 1729 | 1730 | 1731 | 1732 | 1733 | 1734 | 1735 | 1736 | 1737 | 1738 | 1739 | 1740 | 1741 | 1742 | 1743 | 1744 | 1745 | 1746 | 1747 | 1748 | 1749 | 1750 | ############################# 1751 | ## ## 1752 | ## MAIN PROGRAM ## 1753 | ## ## 1754 | ############################# 1755 | 1756 | 1757 | ## Initialize 1758 | initialize(); 1759 | 1760 | ## Process Command Line 1761 | processCommandLine(); 1762 | $conf{'alarm'} = $opt{'timeout'}; 1763 | 1764 | ## Abort program after $conf{'alarm'} seconds to avoid infinite hangs 1765 | alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32 1766 | 1767 | 1768 | 1769 | 1770 | ################################################### 1771 | ## Read $message from STDIN if -m was not used ## 1772 | ################################################### 1773 | 1774 | if (!($message)) { 1775 | ## Read message body from a file specified with -o message-file= 1776 | if ($opt{'message-file'}) { 1777 | if (! -e $opt{'message-file'}) { 1778 | printmsg("ERROR => Message body file specified [$opt{'message-file'}] does not exist!", 0); 1779 | printmsg("HINT => 1) check spelling of your file; 2) fully qualify the path; 3) doubble quote it", 1); 1780 | quit("", 1); 1781 | } 1782 | if (! -r $opt{'message-file'}) { 1783 | printmsg("ERROR => Message body file specified can not be read due to restricted permissions!", 0); 1784 | printmsg("HINT => Check permissions on file specified to ensure it can be read", 1); 1785 | quit("", 1); 1786 | } 1787 | if (!open(MFILE, "< " . $opt{'message-file'})) { 1788 | printmsg("ERROR => Error opening message body file [$opt{'message-file'}]: $!", 0); 1789 | quit("", 1); 1790 | } 1791 | while () { 1792 | $message .= $_; 1793 | } 1794 | close(MFILE); 1795 | } 1796 | 1797 | ## Read message body from STDIN 1798 | else { 1799 | alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32 1800 | if ($conf{'stdout'}) { 1801 | print "Reading message body from STDIN because the '-m' option was not used.\n"; 1802 | print "If you are manually typing in a message:\n"; 1803 | print " - First line must be received within $conf{'alarm'} seconds.\n" if ($^O !~ /win/i); 1804 | print " - End manual input with a CTRL-D on its own line.\n\n" if ($^O !~ /win/i); 1805 | print " - End manual input with a CTRL-Z on its own line.\n\n" if ($^O =~ /win/i); 1806 | } 1807 | while () { ## Read STDIN into $message 1808 | $message .= $_; 1809 | alarm(0) if ($^O !~ /win/i); ## Disable the alarm since at least one line was received 1810 | } 1811 | printmsg("Message input complete.", 0); 1812 | } 1813 | } 1814 | 1815 | ## Replace bare LF's with CRLF's (\012 should always have \015 with it) 1816 | $message =~ s/(\015)?(\012|$)/\015\012/g; 1817 | 1818 | ## Replace bare CR's with CRLF's (\015 should always have \012 with it) 1819 | $message =~ s/(\015)(\012|$)?/\015\012/g; 1820 | 1821 | ## Check message for bare periods and encode them 1822 | $message =~ s/(^|$CRLF)(\.{1})($CRLF|$)/$1.$2$3/g; 1823 | 1824 | ## Get the current date for the email header 1825 | my ($sec,$min,$hour,$mday,$mon,$year,$day) = gmtime(); 1826 | $year += 1900; $mon = return_month($mon); $day = return_day($day); 1827 | my $date = sprintf("%s, %s %s %d %.2d:%.2d:%.2d %s",$day, $mday, $mon, $year, $hour, $min, $sec, $conf{'timezone'}); 1828 | 1829 | 1830 | 1831 | 1832 | ################################## 1833 | ## Connect to the SMTP server ## 1834 | ################################## 1835 | printmsg("DEBUG => Connecting to $conf{'server'}:$conf{'port'}", 1); 1836 | $SIG{'ALRM'} = sub { 1837 | printmsg("ERROR => Timeout while connecting to $conf{'server'}:$conf{'port'} There was no response after $conf{'alarm'} seconds.", 0); 1838 | printmsg("HINT => Try specifying a different mail relay with the -s option.", 1); 1839 | quit("", 1); 1840 | }; 1841 | alarm($conf{'alarm'}) if ($^O !~ /win/i); ## alarm() doesn't work in win32; 1842 | $SERVER = IO::Socket::INET->new( PeerAddr => $conf{'server'}, 1843 | PeerPort => $conf{'port'}, 1844 | LocalAddr => $conf{'bindaddr'}, 1845 | Proto => 'tcp', 1846 | Autoflush => 1, 1847 | timeout => $conf{'alarm'}, 1848 | ); 1849 | alarm(0) if ($^O !~ /win/i); ## alarm() doesn't work in win32; 1850 | 1851 | ## Make sure we got connected 1852 | if ( (!$SERVER) or (!$SERVER->opened()) ) { 1853 | printmsg("ERROR => Connection attempt to $conf{'server'}:$conf{'port'} failed: $@", 0); 1854 | printmsg("HINT => Try specifying a different mail relay with the -s option.", 1); 1855 | quit("", 1); 1856 | } 1857 | 1858 | ## Save our IP address for later 1859 | $conf{'ip'} = $SERVER->sockhost(); 1860 | printmsg("DEBUG => My IP address is: $conf{'ip'}", 1); 1861 | 1862 | 1863 | 1864 | 1865 | 1866 | 1867 | 1868 | ######################### 1869 | ## Do the SMTP Dance ## 1870 | ######################### 1871 | 1872 | ## Read initial greeting to make sure we're talking to a live SMTP server 1873 | if (SMTPchat()) { quit($conf{'error'}, 1); } 1874 | 1875 | ## We're about to use $opt{'fqdn'}, make sure it isn't empty 1876 | if (!$opt{'fqdn'}) { 1877 | ## Ok, that means we couldn't get a hostname, how about using the IP address for the HELO instead 1878 | $opt{'fqdn'} = "[" . $conf{'ip'} . "]"; 1879 | } 1880 | 1881 | ## EHLO 1882 | if (SMTPchat('EHLO ' . $opt{'fqdn'})) { 1883 | printmsg($conf{'error'}, 0); 1884 | printmsg("NOTICE => EHLO command failed, attempting HELO instead"); 1885 | if (SMTPchat('HELO ' . $opt{'fqdn'})) { quit($conf{'error'}, 1); } 1886 | if ( $opt{'username'} and $opt{'password'} ) { 1887 | printmsg("WARNING => The mail server does not support SMTP authentication!", 0); 1888 | } 1889 | } 1890 | else { 1891 | 1892 | ## Determin if the server supports TLS 1893 | if ($conf{'SMTPchat_response'} =~ /STARTTLS/) { 1894 | $conf{'tls_server'} = 1; 1895 | printmsg("DEBUG => The remote SMTP server supports TLS :)", 2); 1896 | } 1897 | else { 1898 | $conf{'tls_server'} = 0; 1899 | printmsg("DEBUG => The remote SMTP server does NOT support TLS :(", 2); 1900 | } 1901 | 1902 | ## Start TLS if possible 1903 | if ($conf{'tls_server'} == 1 and $conf{'tls_client'} == 1 and $opt{'tls'} =~ /^(yes|auto)$/) { 1904 | printmsg("DEBUG => Starting TLS", 2); 1905 | if (SMTPchat('STARTTLS')) { quit($conf{'error'}, 1); } 1906 | if (! IO::Socket::SSL->start_SSL($SERVER, SSL_version => 'SSLv3 TLSv1')) { 1907 | quit("ERROR => TLS setup failed: " . IO::Socket::SSL::errstr(), 1); 1908 | } 1909 | printmsg("DEBUG => TLS: Using cipher: ". $SERVER->get_cipher(), 3); 1910 | printmsg("DEBUG => TLS session initialized :)", 1); 1911 | 1912 | ## Restart our SMTP session 1913 | if (SMTPchat('EHLO ' . $opt{'fqdn'})) { quit($conf{'error'}, 1); } 1914 | } 1915 | elsif ($opt{'tls'} eq 'yes' and $conf{'tls_server'} == 0) { 1916 | quit("ERROR => TLS not possible! Remote SMTP server, $conf{'server'}, does not support it.", 1); 1917 | } 1918 | 1919 | 1920 | ## Do SMTP Auth if required 1921 | if ( $opt{'username'} and $opt{'password'} ) { 1922 | if ($conf{'SMTPchat_response'} !~ /AUTH\s/) { 1923 | printmsg("NOTICE => Authentication not supported by the remote SMTP server!", 0); 1924 | } 1925 | else { 1926 | my $auth_succeeded = 0; 1927 | my $mutual_method = 0; 1928 | 1929 | # ## SASL CRAM-MD5 authentication method 1930 | # if ($conf{'SMTPchat_response'} =~ /\bCRAM-MD5\b/i) { 1931 | # printmsg("DEBUG => SMTP-AUTH: Using CRAM-MD5 authentication method", 1); 1932 | # if (SMTPchat('AUTH CRAM-MD5')) { quit($conf{'error'}, 1); } 1933 | # 1934 | # ## FIXME!! 1935 | # 1936 | # printmsg("DEBUG => User authentication was successful", 1); 1937 | # } 1938 | 1939 | ## SASL LOGIN authentication method 1940 | if ($auth_succeeded == 0 and $conf{'SMTPchat_response'} =~ /\bLOGIN\b/i) { 1941 | $mutual_method = 1; 1942 | printmsg("DEBUG => SMTP-AUTH: Using LOGIN authentication method", 1); 1943 | if (!SMTPchat('AUTH LOGIN')) { 1944 | if (!SMTPchat(base64_encode($opt{'username'}))) { 1945 | if (!SMTPchat(base64_encode($opt{'password'}))) { 1946 | $auth_succeeded = 1; 1947 | printmsg("DEBUG => User authentication was successful (Method: LOGIN)", 1); 1948 | } 1949 | } 1950 | } 1951 | if ($auth_succeeded == 0) { 1952 | printmsg("DEBUG => SMTP-AUTH: LOGIN authenticaion failed.", 1); 1953 | } 1954 | } 1955 | 1956 | ## SASL PLAIN authentication method 1957 | if ($auth_succeeded == 0 and $conf{'SMTPchat_response'} =~ /\bPLAIN\b/i) { 1958 | $mutual_method = 1; 1959 | printmsg("DEBUG => SMTP-AUTH: Using PLAIN authentication method", 1); 1960 | if (SMTPchat('AUTH PLAIN ' . base64_encode("$opt{'username'}\0$opt{'username'}\0$opt{'password'}"))) { 1961 | printmsg("DEBUG => SMTP-AUTH: PLAIN authenticaion failed.", 1); 1962 | } 1963 | else { 1964 | $auth_succeeded = 1; 1965 | printmsg("DEBUG => User authentication was successful (Method: PLAIN)", 1); 1966 | } 1967 | } 1968 | 1969 | ## If none of the authentication methods supported by sendEmail were supported by the server, let the user know 1970 | if ($mutual_method == 0) { 1971 | printmsg("WARNING => SMTP-AUTH: No mutually supported authentication methods available", 0); 1972 | } 1973 | 1974 | ## If we didn't get authenticated, log an error message and exit 1975 | if ($auth_succeeded == 0) { 1976 | quit("ERROR => ERROR => SMTP-AUTH: Authentication to $conf{'server'}:$conf{'port'} failed.", 1); 1977 | } 1978 | } 1979 | } 1980 | } 1981 | 1982 | ## MAIL FROM 1983 | if (SMTPchat('MAIL FROM:<' .(returnAddressParts($from))[1]. '>')) { quit($conf{'error'}, 1); } 1984 | 1985 | ## RCPT TO 1986 | my $oneRcptAccepted = 0; 1987 | foreach my $rcpt (@to, @cc, @bcc) { 1988 | my ($name, $address) = returnAddressParts($rcpt); 1989 | if (SMTPchat('RCPT TO:<' . $address . '>')) { 1990 | printmsg("WARNING => The recipient <$address> was rejected by the mail server, error follows:", 0); 1991 | $conf{'error'} =~ s/^ERROR/WARNING/o; 1992 | printmsg($conf{'error'}, 0); 1993 | } 1994 | elsif ($oneRcptAccepted == 0) { 1995 | $oneRcptAccepted = 1; 1996 | } 1997 | } 1998 | ## If no recipients were accepted we need to exit with an error. 1999 | if ($oneRcptAccepted == 0) { 2000 | quit("ERROR => Exiting. No recipients were accepted for delivery by the mail server.", 1); 2001 | } 2002 | 2003 | ## DATA 2004 | if (SMTPchat('DATA')) { quit($conf{'error'}, 1); } 2005 | 2006 | 2007 | ############################### 2008 | ## Build and send the body ## 2009 | ############################### 2010 | printmsg("INFO => Sending message body",1); 2011 | 2012 | ## If the message-format is raw just send the message as-is. 2013 | if ($opt{'message-format'} =~ /^raw$/i) { 2014 | print $SERVER $message; 2015 | } 2016 | 2017 | ## If the message-format isn't raw, then build and send the message, 2018 | else { 2019 | 2020 | ## Message-ID: 2021 | if ($opt{'message-header'} !~ /^Message-ID:/iom) { 2022 | $header .= 'Message-ID: <' . $conf{'Message-ID'} . '@' . $conf{'hostname'} . '>' . $CRLF; 2023 | } 2024 | 2025 | ## From: "Name" (the pointless test below is just to keep scoping correct) 2026 | if ($from and $opt{'message-header'} !~ /^From:/iom) { 2027 | my ($name, $address) = returnAddressParts($from); 2028 | $header .= 'From: "' . $name . '" <' . $address . '>' . $CRLF; 2029 | } 2030 | 2031 | ## Reply-To: 2032 | if ($opt{'reply-to'} and $opt{'message-header'} !~ /^Reply-To:/iom) { 2033 | my ($name, $address) = returnAddressParts($opt{'reply-to'}); 2034 | $header .= 'Reply-To: "' . $name . '" <' . $address . '>' . $CRLF; 2035 | } 2036 | 2037 | ## To: "Name" 2038 | if ($opt{'message-header'} =~ /^To:/iom) { 2039 | ## The user put the To: header in via -o message-header - dont do anything 2040 | } 2041 | elsif (scalar(@to) > 0) { 2042 | $header .= "To:"; 2043 | for (my $a = 0; $a < scalar(@to); $a++) { 2044 | my $msg = ""; 2045 | 2046 | my ($name, $address) = returnAddressParts($to[$a]); 2047 | $msg = " \"$name\" <$address>"; 2048 | 2049 | ## If we're not on the last address add a comma to the end of the line. 2050 | if (($a + 1) != scalar(@to)) { 2051 | $msg .= ","; 2052 | } 2053 | 2054 | $header .= $msg . $CRLF; 2055 | } 2056 | } 2057 | ## We always want a To: line so if the only recipients were bcc'd they don't see who it was sent to 2058 | else { 2059 | $header .= "To: \"Undisclosed Recipients\" <>$CRLF"; 2060 | } 2061 | 2062 | if (scalar(@cc) > 0 and $opt{'message-header'} !~ /^Cc:/iom) { 2063 | $header .= "Cc:"; 2064 | for (my $a = 0; $a < scalar(@cc); $a++) { 2065 | my $msg = ""; 2066 | 2067 | my ($name, $address) = returnAddressParts($cc[$a]); 2068 | $msg = " \"$name\" <$address>"; 2069 | 2070 | ## If we're not on the last address add a comma to the end of the line. 2071 | if (($a + 1) != scalar(@cc)) { 2072 | $msg .= ","; 2073 | } 2074 | 2075 | $header .= $msg . $CRLF; 2076 | } 2077 | } 2078 | 2079 | if ($opt{'message-header'} !~ /^Subject:/iom) { 2080 | $header .= 'Subject: ' . $subject . $CRLF; ## Subject 2081 | } 2082 | if ($opt{'message-header'} !~ /^Date:/iom) { 2083 | $header .= 'Date: ' . $date . $CRLF; ## Date 2084 | } 2085 | if ($opt{'message-header'} !~ /^X-Mailer:/iom) { 2086 | $header .= 'X-Mailer: sendEmail-'.$conf{'version'}.$CRLF; ## X-Mailer 2087 | } 2088 | ## I wonder if I should put this in by default? 2089 | # if ($opt{'message-header'} !~ /^X-Originating-IP:/iom) { 2090 | # $header .= 'X-Originating-IP: ['.$conf{'ip'}.']'.$CRLF; ## X-Originating-IP 2091 | # } 2092 | 2093 | ## Encode all messages with MIME. 2094 | if ($opt{'message-header'} !~ /^MIME-Version:/iom) { 2095 | $header .= "MIME-Version: 1.0$CRLF"; 2096 | } 2097 | if ($opt{'message-header'} !~ /^Content-Type:/iom) { 2098 | my $content_type = 'multipart/mixed'; 2099 | if (scalar(@attachments) == 0) { $content_type = 'multipart/related'; } 2100 | $header .= "Content-Type: $content_type; boundary=\"$conf{'delimiter'}\"$CRLF"; 2101 | } 2102 | 2103 | ## Send additional message header line(s) if specified 2104 | if ($opt{'message-header'}) { 2105 | $header .= $opt{'message-header'}; 2106 | } 2107 | 2108 | ## Send the message header to the server 2109 | print $SERVER $header . $CRLF; 2110 | 2111 | ## Start sending the message body to the server 2112 | print $SERVER "This is a multi-part message in MIME format. To properly display this message you need a MIME-Version 1.0 compliant Email program.$CRLF"; 2113 | print $SERVER "$CRLF"; 2114 | 2115 | 2116 | ## Send message body 2117 | print $SERVER "--$conf{'delimiter'}$CRLF"; 2118 | ## Send a message content-type header: 2119 | ## If the message contains HTML... 2120 | if ($opt{'message-content-type'} eq 'html' or ($opt{'message-content-type'} eq 'auto' and $message =~ /^\s*( 0) { 2142 | ## Disable the alarm so people on modems can send big attachments 2143 | alarm(0) if ($^O !~ /win/i); ## alarm() doesn't work in win32 2144 | 2145 | ## Send the attachments 2146 | foreach my $filename (@attachments) { 2147 | ## This is check 2, we already checked this above, but just in case... 2148 | if ( ! -f $filename ) { 2149 | printmsg("ERROR => The file [$filename] doesn't exist! Email will be sent, but without that attachment.", 0); 2150 | } 2151 | elsif ( ! -r $filename ) { 2152 | printmsg("ERROR => Couldn't open the file [$filename] for reading: $! Email will be sent, but without that attachment.", 0); 2153 | } 2154 | else { 2155 | printmsg("DEBUG => Sending the attachment [$filename]", 1); 2156 | send_attachment($filename); 2157 | } 2158 | } 2159 | } 2160 | 2161 | 2162 | ## End the mime encoded message 2163 | print $SERVER "$CRLF--$conf{'delimiter'}--$CRLF"; 2164 | } 2165 | 2166 | 2167 | ## Tell the server we are done sending the email 2168 | print $SERVER "$CRLF.$CRLF"; 2169 | if (SMTPchat()) { quit($conf{'error'}, 1); } 2170 | 2171 | 2172 | 2173 | #################### 2174 | # We are done!!! # 2175 | #################### 2176 | 2177 | ## Disconnect from the server (don't SMTPchat(), it breaks when using TLS) 2178 | print $SERVER "QUIT$CRLF"; 2179 | close $SERVER; 2180 | 2181 | 2182 | 2183 | 2184 | 2185 | 2186 | ####################################### 2187 | ## Generate exit message/log entry ## 2188 | ####################################### 2189 | 2190 | if ($conf{'debug'} or $conf{'logging'}) { 2191 | printmsg("Generating a detailed exit message", 3); 2192 | 2193 | ## Put the message together 2194 | my $output = "Email was sent successfully! From: <" . (returnAddressParts($from))[1] . "> "; 2195 | 2196 | if (scalar(@to) > 0) { 2197 | $output .= "To: "; 2198 | for ($a = 0; $a < scalar(@to); $a++) { 2199 | $output .= "<" . (returnAddressParts($to[$a]))[1] . "> "; 2200 | } 2201 | } 2202 | if (scalar(@cc) > 0) { 2203 | $output .= "Cc: "; 2204 | for ($a = 0; $a < scalar(@cc); $a++) { 2205 | $output .= "<" . (returnAddressParts($cc[$a]))[1] . "> "; 2206 | } 2207 | } 2208 | if (scalar(@bcc) > 0) { 2209 | $output .= "Bcc: "; 2210 | for ($a = 0; $a < scalar(@bcc); $a++) { 2211 | $output .= "<" . (returnAddressParts($bcc[$a]))[1] . "> "; 2212 | } 2213 | } 2214 | $output .= "Subject: [$subject] " if ($subject); 2215 | if (scalar(@attachments_names) > 0) { 2216 | $output .= "Attachment(s): "; 2217 | foreach(@attachments_names) { 2218 | $output .= "[$_] "; 2219 | } 2220 | } 2221 | $output .= "Server: [$conf{'server'}:$conf{'port'}]"; 2222 | 2223 | 2224 | ###################### 2225 | # Exit the program # 2226 | ###################### 2227 | 2228 | ## Print / Log the detailed message 2229 | quit($output, 0); 2230 | } 2231 | else { 2232 | ## Or the standard message 2233 | quit("Email was sent successfully!", 0); 2234 | } 2235 | -------------------------------------------------------------------------------- /container-files/usr/local/share/zabbix/alertscripts/zabbix_notifications.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -X POST\ 4 | -H "Content-type:application/json"\ 5 | -d "{Id:'$1', text:'$2', triggerId:'$3', playSound:true}"\ 6 | http://zabbkit.inside.cactussoft.biz/api/messages -------------------------------------------------------------------------------- /container-files/usr/local/share/zabbix/alertscripts/zabbix_sendmail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export MAIL_FROM=default@domain.com 3 | export MAIL_TO=$1 4 | export MAIL_SUBJECT=$2 5 | export MAIL_BODY=$3 6 | export MAIL_SMTP_SERVER=default.smtp.server.com 7 | export MAIL_SMTP_USER=default.smtp.username 8 | export MAIL_SMTP_PASS=default.smtp.password 9 | /usr/bin/sendmail -f ${MAIL_FROM} -t ${MAIL_TO} -u ${MAIL_SUBJECT} -m ${MAIL_BODY} -s ${MAIL_SMTP_SERVER} -xu ${MAIL_SMTP_USER} -xp ${MAIL_SMTP_PASS} -o tls=no -------------------------------------------------------------------------------- /container-files/usr/local/share/zabbix/alertscripts/zabbix_slack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Slack incoming web-hook URL and user name 4 | WEBHOOK='SLACK_WEBHOOK' 5 | USER='Zabbix' 6 | CHANNEL=${1} 7 | SUBJECT=${2} 8 | # Emoji selection based on Subject 9 | if [ "${SUBJECT}" == 'OK' ]; then 10 | EMOJI=':white_check_mark:' 11 | elif [ "${SUBJECT}" == 'PROBLEM' ]; then 12 | EMOJI=':bangbang:' 13 | else 14 | EMOJI=':slack:' 15 | fi 16 | # Prepare message 17 | MESSAGE="${SUBJECT}: $3" 18 | 19 | SLACK_PAYLOAD="payload={\"channel\": \"${CHANNEL}\", \"text\": \"${MESSAGE}\", \"icon_emoji\": \"${EMOJI}\"}" 20 | # Send to slack 21 | curl -m 5 --data-urlencode "${SLACK_PAYLOAD}" ${WEBHOOK} 22 | -------------------------------------------------------------------------------- /container-files/usr/local/share/zabbix/externalscripts/getFPMInfo.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | ### Przemyslaw Ozgo (2015) 3 | # Thanks to Vicente Dominguez 4 | # 5 | # Options: 6 | # 7 | # -a "accepted conn" 8 | # -a "max listen queue" 9 | # -a "listen queue len" 10 | # -a "idle processes" 11 | # -a "active processes" 12 | # -a "total processes" 13 | # -a "slow requests" 14 | # 15 | 16 | import simplejson as json 17 | import urllib2, base64, sys, getopt 18 | import re 19 | 20 | ## 21 | 22 | def Usage (): 23 | print "Usage: getPhpInfo.py -h 127.0.0.1 -p 80 -a [accepted conn|max listen queue|listen queue len|idle processes|active processes|total processes|slow requests]" 24 | sys.exit(2) 25 | 26 | ## 27 | 28 | def main (): 29 | 30 | # Default values 31 | host = "localhost" 32 | port = "80" 33 | getInfo = "None" 34 | 35 | if len(sys.argv) < 2: 36 | Usage() 37 | 38 | try: 39 | opts, args = getopt.getopt(sys.argv[1:], "h:p:a:") 40 | except getopt.GetoptError: 41 | Usage() 42 | 43 | # Assign parameters as variables 44 | for opt, arg in opts : 45 | if opt == "-h" : 46 | host = arg 47 | if opt == "-p" : 48 | port = arg 49 | if opt == "-a" : 50 | getInfo = arg 51 | 52 | url="http://" + host + ":" + port + "/fpm_status?json" 53 | request = urllib2.Request(url) 54 | try: 55 | result = urllib2.urlopen(request) 56 | buffer = json.loads(result.read()) 57 | except: 58 | print "-1" 59 | sys.exit(1) 60 | print buffer.pop(getInfo,'unknown') 61 | sys.exit(0) 62 | 63 | if __name__ == "__main__": 64 | main() -------------------------------------------------------------------------------- /container-files/usr/local/share/zabbix/externalscripts/getNginxInfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ### Vicente Dominguez 3 | # 4 | # Options: 5 | # 6 | # -a active 7 | # -a accepted 8 | # -a handled 9 | # -a requests 10 | # -a reading 11 | # -a writting 12 | # -a waiting 13 | # 14 | 15 | import urllib2, base64, sys, getopt 16 | import re 17 | 18 | ## 19 | 20 | 21 | def Usage(): 22 | print "Usage: getWowzaInfo.py -h 127.0.0.1 -p 80 -a [active|accepted|handled|request|reading|writting|waiting]" 23 | sys.exit(2) 24 | 25 | ## 26 | 27 | 28 | def main(): 29 | 30 | # Default values 31 | host = "localhost" 32 | port = "80" 33 | getInfo = "None" 34 | ssl = "" 35 | 36 | if len(sys.argv) < 2: 37 | Usage() 38 | 39 | try: 40 | opts, args = getopt.getopt(sys.argv[1:], "h:p:a:") 41 | except getopt.GetoptError: 42 | Usage() 43 | 44 | # Assign parameters as variables 45 | for opt, arg in opts: 46 | if opt == "-h": 47 | host = arg 48 | if opt == "-p": 49 | port = arg 50 | if opt == "-a": 51 | getInfo = arg 52 | # Select SSL connection of potr is 443. 53 | if port == "443" : 54 | ssl = "s" 55 | 56 | url = "http" + ssl + "://" + host + ":" + port + "/nginx_status/" 57 | request = urllib2.Request(url) 58 | result = urllib2.urlopen(request) 59 | 60 | buffer = re.findall(r'\d{1,8}', result.read()) 61 | 62 | ## Format: 63 | ## Active connections: 196 64 | ## server accepts handled requests 65 | ## 272900 272900 328835 66 | ## Reading: 0 Writing: 6 Waiting: 190 67 | 68 | if (getInfo == "active"): 69 | print buffer[0] 70 | elif (getInfo == "accepted"): 71 | print buffer[1] 72 | elif (getInfo == "handled"): 73 | print buffer[2] 74 | elif (getInfo == "requests"): 75 | print buffer[3] 76 | elif (getInfo == "reading"): 77 | print buffer[4] 78 | elif (getInfo == "writting"): 79 | print buffer[5] 80 | elif (getInfo == "waiting"): 81 | print buffer[6] 82 | else: 83 | print "unknown" 84 | sys.exit(1) 85 | 86 | 87 | if __name__ == "__main__": 88 | main() 89 | -------------------------------------------------------------------------------- /docker-cloud.yml: -------------------------------------------------------------------------------- 1 | zabbix-db: 2 | image: million12/mariadb 3 | restart: always 4 | environment: 5 | - MARIADB_USER=admin 6 | - MARIADB_PASS=sgTJKf3K8AqCNz 7 | 8 | zabbix: 9 | image: million12/zabbix-server:latest 10 | restart: always 11 | links: 12 | - zabbix-db 13 | ports: 14 | - "80:80" 15 | - "10051:10051" 16 | environment: 17 | - ZS_DBHost=zabbix-db 18 | - ZS_DBUser=admin 19 | - ZS_DBPassword=sgTJKf3K8AqCNz 20 | -------------------------------------------------------------------------------- /images/actions1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/million12/docker-zabbix-server/e5a86f18d408553fc1821a7c3a0865a7d9c6ce48/images/actions1.jpg -------------------------------------------------------------------------------- /images/actions2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/million12/docker-zabbix-server/e5a86f18d408553fc1821a7c3a0865a7d9c6ce48/images/actions2.jpg -------------------------------------------------------------------------------- /images/media-type.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/million12/docker-zabbix-server/e5a86f18d408553fc1821a7c3a0865a7d9c6ce48/images/media-type.jpg -------------------------------------------------------------------------------- /images/nginx1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/million12/docker-zabbix-server/e5a86f18d408553fc1821a7c3a0865a7d9c6ce48/images/nginx1.jpg -------------------------------------------------------------------------------- /images/nginx2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/million12/docker-zabbix-server/e5a86f18d408553fc1821a7c3a0865a7d9c6ce48/images/nginx2.jpg -------------------------------------------------------------------------------- /images/php-fpm-stats.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/million12/docker-zabbix-server/e5a86f18d408553fc1821a7c3a0865a7d9c6ce48/images/php-fpm-stats.jpg -------------------------------------------------------------------------------- /images/user-media.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/million12/docker-zabbix-server/e5a86f18d408553fc1821a7c3a0865a7d9c6ce48/images/user-media.jpg -------------------------------------------------------------------------------- /templates-files/zabbix-php-fpm-template.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2.0 4 | 2015-06-16T21:36:00Z 5 | 6 | 7 | Templates 8 | 9 | 10 | 11 | 380 | 381 | 382 | 383 | {PHP-FPM Service:net.tcp.service[tcp,,9000].count(#3,0,"eq")}=3 384 | Php port 9000 is not running 385 | 386 | 0 387 | 5 388 | 389 | 0 390 | 391 | 392 | 393 | 394 | 395 | php-fpm Accepted Connections /sec 396 | 900 397 | 200 398 | 0.0000 399 | 100.0000 400 | 1 401 | 1 402 | 0 403 | 1 404 | 0 405 | 0.0000 406 | 0.0000 407 | 0 408 | 0 409 | 0 410 | 0 411 | 412 | 413 | 0 414 | 0 415 | C80000 416 | 0 417 | 2 418 | 0 419 | 420 | PHP-FPM Service 421 | getFPMInfo.py["-h","{HOST.CONN}","-p","80","-a","accepted conn"] 422 | 423 | 424 | 425 | 426 | 427 | php-fpm Listen Queue 428 | 900 429 | 200 430 | 0.0000 431 | 100.0000 432 | 1 433 | 1 434 | 0 435 | 1 436 | 0 437 | 0.0000 438 | 0.0000 439 | 0 440 | 0 441 | 0 442 | 0 443 | 444 | 445 | 0 446 | 0 447 | EE0000 448 | 0 449 | 2 450 | 0 451 | 452 | PHP-FPM Service 453 | getFPMInfo.py["-h","{HOST.CONN}","-p","80","-a","listen queue len"] 454 | 455 | 456 | 457 | 1 458 | 0 459 | 00EE00 460 | 0 461 | 2 462 | 0 463 | 464 | PHP-FPM Service 465 | getFPMInfo.py["-h","{HOST.CONN}","-p","80","-a","max listen queue"] 466 | 467 | 468 | 469 | 470 | 471 | php-fpm Processes 472 | 900 473 | 200 474 | 0.0000 475 | 100.0000 476 | 1 477 | 1 478 | 0 479 | 1 480 | 0 481 | 0.0000 482 | 0.0000 483 | 0 484 | 0 485 | 0 486 | 0 487 | 488 | 489 | 0 490 | 0 491 | C80000 492 | 0 493 | 2 494 | 0 495 | 496 | PHP-FPM Service 497 | getFPMInfo.py["-h","{HOST.CONN}","-p","80","-a","total processes"] 498 | 499 | 500 | 501 | 1 502 | 0 503 | 00C800 504 | 0 505 | 2 506 | 0 507 | 508 | PHP-FPM Service 509 | getFPMInfo.py["-h","{HOST.CONN}","-p","80","-a","active processes"] 510 | 511 | 512 | 513 | 2 514 | 0 515 | 0000C8 516 | 0 517 | 2 518 | 0 519 | 520 | PHP-FPM Service 521 | getFPMInfo.py["-h","{HOST.CONN}","-p","80","-a","idle processes"] 522 | 523 | 524 | 525 | 526 | 527 | php-fpm Slow Requests / sec 528 | 900 529 | 200 530 | 0.0000 531 | 100.0000 532 | 1 533 | 1 534 | 0 535 | 1 536 | 0 537 | 0.0000 538 | 0.0000 539 | 0 540 | 0 541 | 0 542 | 0 543 | 544 | 545 | 0 546 | 0 547 | C80000 548 | 0 549 | 2 550 | 0 551 | 552 | PHP-FPM Service 553 | getFPMInfo.py["-h","{HOST.CONN}","-p","80","-a","slow requests"] 554 | 555 | 556 | 557 | 558 | 559 | -------------------------------------------------------------------------------- /templates-files/zbx_nginx_template.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2.0 4 | 2014-12-05T13:38:10Z 5 | 6 | 7 | Templates 8 | 9 | 10 | 11 | 300 | 301 | 302 | 303 | Connection Stats 304 | 900 305 | 200 306 | 0.0000 307 | 100.0000 308 | 1 309 | 1 310 | 0 311 | 1 312 | 0 313 | 0.0000 314 | 0.0000 315 | 0 316 | 0 317 | 0 318 | 0 319 | 320 | 321 | 0 322 | 0 323 | 0000C8 324 | 0 325 | 2 326 | 0 327 | 328 | Template Nginx Info 329 | getNginxInfo.py["-h","{HOST.CONN}","-p","80","-a","accepted"] 330 | 331 | 332 | 333 | 1 334 | 0 335 | 00C800 336 | 0 337 | 2 338 | 0 339 | 340 | Template Nginx Info 341 | getNginxInfo.py["-h","{HOST.CONN}","-p","80","-a","handled"] 342 | 343 | 344 | 345 | 2 346 | 0 347 | C80000 348 | 0 349 | 2 350 | 0 351 | 352 | Template Nginx Info 353 | getNginxInfo.py["-h","{HOST.CONN}","-p","80","-a","requests"] 354 | 355 | 356 | 357 | 358 | 359 | Connection Status 360 | 900 361 | 200 362 | 0.0000 363 | 100.0000 364 | 1 365 | 1 366 | 0 367 | 1 368 | 0 369 | 0.0000 370 | 0.0000 371 | 0 372 | 0 373 | 0 374 | 0 375 | 376 | 377 | 0 378 | 0 379 | 00C800 380 | 0 381 | 2 382 | 0 383 | 384 | Template Nginx Info 385 | getNginxInfo.py["-h","{HOST.CONN}","-p","80","-a","active"] 386 | 387 | 388 | 389 | 1 390 | 0 391 | 0000C8 392 | 0 393 | 2 394 | 0 395 | 396 | Template Nginx Info 397 | getNginxInfo.py["-h","{HOST.CONN}","-p","80","-a","reading"] 398 | 399 | 400 | 401 | 2 402 | 0 403 | C80000 404 | 0 405 | 2 406 | 0 407 | 408 | Template Nginx Info 409 | getNginxInfo.py["-h","{HOST.CONN}","-p","80","-a","waiting"] 410 | 411 | 412 | 413 | 3 414 | 0 415 | C800C8 416 | 0 417 | 2 418 | 0 419 | 420 | Template Nginx Info 421 | getNginxInfo.py["-h","{HOST.CONN}","-p","80","-a","writting"] 422 | 423 | 424 | 425 | 426 | 427 | --------------------------------------------------------------------------------