├── version.txt ├── .gitignore ├── .gitattributes ├── README.md ├── hMailServerBackup.ps1 ├── hMailServerBackupDownload.ps1 ├── hMailServerBackupConfig.dist.ps1 ├── hMailServerBackupPruneMessagesTEST.ps1 └── hMailServerBackupFunctions.ps1 /version.txt: -------------------------------------------------------------------------------- 1 | 1.23 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore config file 2 | /hMailServerBackupConfig.ps1 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hMailServer Offsite Backup 2 | hMailServer backup routine with extras 3 | 4 | Discussion thread: https://hmailserver.com/forum/viewtopic.php?f=9&t=35447 5 | 6 | # NEW 7 | OpenPhish database files updater. New variables added to config. 8 | See topic for further info: https://hmailserver.com/forum/viewtopic.php?t=40295 9 | 10 | Removed offsite upload to letsupload.io because they are now defunct 11 | 12 | 13 | # What does it do? 14 | 1) Stops hMailServer and SpamAssassin Services 15 | 2) Updates SpamAssassin 16 | 3) Cycles hMailServer and SpamAssassin logs 17 | 4) Backs up hMailServer data directory using RoboCopy 18 | 5) Dumps MySQL database -or- internal database and adds to backup directory 19 | 6) Backs up miscellaneous files 20 | 7) Updates OpenPhish database files 21 | 8) Restarts SpamAssassin and hMailServer 22 | 9) Prunes messages older than specified number of days in specified folders and subfolders (eg Trash, Spam, etc) 23 | 10) Feeds messages newer than specified number of days to Bayes database through spamc.exe 24 | 11) Prunes hMailServer logs older than specified number of days 25 | 12) Prunes local backup copies older than specified number of days 26 | 13) Compresses the backup directory into a multi-volume 7z archive with AES-256 encryption (including header encyrption) 27 | 14) Sends email with debug log attached 28 | 29 | # Requirements 30 | Working hMailServer using either internal database or MySQL 31 | If using SpamAssassin, must configure service for --allow-tell in order to feed spamc 32 | OpenPhish update requires WGET in the system path 33 | 34 | # Instructions 35 | Create account at LetsUpload.io and create API keys 36 | Fill in variables in hMailServerBackupConfig.ps1.dist and rename to hMailServerBackupConfig.ps1 37 | Run hMailServerBackup.ps1 from task scheduler at 11:58 PM (time allows for properly cycling logs) 38 | 39 | # Notes 40 | -------------------------------------------------------------------------------- /hMailServerBackup.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .SYNOPSIS 4 | hMailServer Backup 5 | 6 | .DESCRIPTION 7 | hMailServer Backup 8 | 9 | .FUNCTIONALITY 10 | Backs up hMailServer, compresses backup and uploads to LetsUpload.io 11 | 12 | .PARAMETER 13 | 14 | 15 | .NOTES 16 | Run at 11:58PM from task scheduler in order to properly cycle log files. 17 | 18 | .EXAMPLE 19 | 20 | 21 | #> 22 | 23 | <### LOAD SUPPORTING FILES ###> 24 | Try { 25 | .("$PSScriptRoot\hMailServerBackupConfig.ps1") 26 | .("$PSScriptRoot\hMailServerBackupFunctions.ps1") 27 | } 28 | Catch { 29 | Write-Output "$(Get-Date -f G) : ERROR : Unable to load supporting PowerShell Scripts" | Out-File "$PSScriptRoot\PSError.log" -Append 30 | Write-Output "$(Get-Date -f G) : ERROR : $($Error[0])" | Out-File "$PSScriptRoot\PSError.log" -Append 31 | Exit 32 | } 33 | 34 | <### BEGIN SCRIPT ###> 35 | $StartScript = Get-Date 36 | $DateString = (Get-Date).ToString("yyyy-MM-dd") 37 | $BackupName = "$DateString-hMailServer" 38 | 39 | <# Clear out error variable #> 40 | $Error.Clear() 41 | 42 | <# Set counting variables that pass through functions #> 43 | Set-Variable -Name BackupSuccess -Value 0 -Option AllScope 44 | Set-Variable -Name DoBackupDataDir -Value 0 -Option AllScope 45 | Set-Variable -Name DoBackupDB -Value 0 -Option AllScope 46 | Set-Variable -Name MiscBackupSuccess -Value 0 -Option AllScope 47 | Set-Variable -Name TotalDeletedMessages -Value 0 -Option AllScope 48 | Set-Variable -Name TotalDeletedFolders -Value 0 -Option AllScope 49 | Set-Variable -Name DeleteMessageErrors -Value 0 -Option AllScope 50 | Set-Variable -Name DeleteFolderErrors -Value 0 -Option AllScope 51 | Set-Variable -Name TotalHamFedMessages -Value 0 -Option AllScope 52 | Set-Variable -Name TotalSpamFedMessages -Value 0 -Option AllScope 53 | Set-Variable -Name HamFedMessageErrors -Value 0 -Option AllScope 54 | Set-Variable -Name SpamFedMessageErrors -Value 0 -Option AllScope 55 | Set-Variable -Name LearnedHamMessages -Value 0 -Option AllScope 56 | Set-Variable -Name LearnedSpamMessages -Value 0 -Option AllScope 57 | 58 | <# Remove trailing slashes from folder locations #> 59 | $hMSDir = $hMSDir -Replace('\\$','') 60 | $SevenZipDir = $SevenZipDir -Replace('\\$','') 61 | $MailDataDir = $MailDataDir -Replace('\\$','') 62 | $BackupTempDir = $BackupTempDir -Replace('\\$','') 63 | $BackupLocation = $BackupLocation -Replace('\\$','') 64 | $SADir = $SADir -Replace('\\$','') 65 | $SAConfDir = $SAConfDir -Replace('\\$','') 66 | $MySQLBINdir = $MySQLBINdir -Replace('\\$','') 67 | 68 | <# Validate folders #> 69 | ValidateFolders $hMSDir 70 | ValidateFolders $SevenZipDir 71 | ValidateFolders $MailDataDir 72 | ValidateFolders $BackupTempDir 73 | ValidateFolders $BackupLocation 74 | If ($UseSA) { 75 | ValidateFolders $SADir 76 | ValidateFolders $SAConfDir 77 | } 78 | If ($UseMySQL) { 79 | ValidateFolders $MySQLBINdir 80 | } 81 | 82 | <# Create hMailData folder if it doesn't exist #> 83 | If (-not(Test-Path "$BackupTempDir\hMailData" -PathType Container)) {md "$BackupTempDir\hMailData"} 84 | 85 | <# Delete old debug files and create new #> 86 | $EmailBody = "$PSScriptRoot\EmailBody.log" 87 | If (Test-Path $EmailBody) {Remove-Item -Force -Path $EmailBody} 88 | New-Item $EmailBody 89 | $DebugLog = "$BackupLocation\hMailServerDebug-$DateString.log" 90 | If (Test-Path $DebugLog) {Remove-Item -Force -Path $DebugLog} 91 | New-Item $DebugLog 92 | Write-Output "::: hMailServer Backup Routine $(Get-Date -f D) :::" | Out-File $DebugLog -Encoding ASCII -Append 93 | Write-Output " " | Out-File $DebugLog -Encoding ASCII -Append 94 | If ($UseHTML) { 95 | Write-Output " 96 | 97 | 98 | 99 | " | Out-File $EmailBody -Encoding ASCII -Append 100 | } 101 | 102 | <# Authenticate hMailServer COM #> 103 | $hMS = New-Object -COMObject hMailServer.Application 104 | $hMS.Authenticate("Administrator", $hMSAdminPass) | Out-Null 105 | 106 | <# Get hMailServer Status #> 107 | $BootTime = [DateTime]::ParseExact((((Get-WmiObject -Class win32_operatingsystem).LastBootUpTime).Split(".")[0]), 'yyyyMMddHHmmss', $null) 108 | $hMSStartTime = $hMS.Status.StartTime 109 | $hMSSpamCount = $hMS.Status.RemovedSpamMessages 110 | $hMSVirusCount = $hMS.Status.RemovedViruses 111 | Debug "Last Reboot Time : $(($BootTime).ToString('yyyy-MM-dd HH:mm:ss')) ($(ElapsedTime $BootTime))" 112 | Debug "HMS Start Time : $hMSStartTime ($(ElapsedTime $hMSStartTime))" 113 | Debug "HMS Daily Spam Reject : $hMSSpamCount" 114 | Debug "HMS Daily Viruses Removed : $hMSVirusCount" 115 | If ($UseHTML) { 116 | Email "
:::   hMailServer Backup Routine   :::
" 117 | Email "
$(Get-Date -f D)
" 118 | Email " " 119 | Email "Last Reboot Time: $(($BootTime).ToString('yyyy-MM-dd HH:mm:ss'))" 120 | Email "HMS Start Time: $hMSStartTime" 121 | If ($hMSSpamCount -gt 0) { 122 | Email "HMS Daily Spam Reject count: $hMSSpamCount" 123 | } Else { 124 | Email "HMS Daily Spam Reject count: 0" 125 | } 126 | If ($hMSVirusCount -gt 0) { 127 | Email "HMS Daily Viruses Removed count: $hMSVirusCount" 128 | } Else { 129 | Email "HMS Daily Viruses Removed count: 0" 130 | } 131 | Email " " 132 | } Else { 133 | Email "::: hMailServer Backup Routine :::" 134 | Email " $(Get-Date -f D)" 135 | Email " " 136 | Email "Last Reboot Time: $(($BootTime).ToString('yyyy-MM-dd HH:mm:ss'))" 137 | Email "HMS Start Time: $hMSStartTime" 138 | Email "HMS Daily Spam Reject count: $hMSSpamCount" 139 | Email "HMS Daily Viruses Removed count: $hMSVirusCount" 140 | Email " " 141 | } 142 | 143 | <# Stop hMailServer & SpamAssassin services #> 144 | $BeginShutdownPeriod = Get-Date 145 | ServiceStop $hMSServiceName 146 | If ($UseSA) {ServiceStop $SAServiceName} 147 | 148 | <# Cycle Logs #> 149 | If ($CycleLogs) {CycleLogs} 150 | 151 | <# Update SpamAssassin #> 152 | If ($UseSA) {UpdateSpamassassin} 153 | 154 | <# Update Custom Rulesets #> 155 | If ($UseSA) {If ($UseCustomRuleSets) {UpdateCustomRulesets}} 156 | 157 | <# Update OpenPhish database files #> 158 | If ($UseSA) {If ($UseOpenPhish) {DownloadPhishFiles}} 159 | 160 | <# Backup files using RoboCopy #> 161 | If ($BackupDataDir) {BackuphMailDataDir} 162 | 163 | <# Backup database files #> 164 | If ($BackupDB) {BackupDatabases} 165 | 166 | <# Backup Miscellaneous Files #> 167 | If ($BackupMisc) {BackupMiscellaneousFiles} 168 | 169 | <# Report backup success #> 170 | Debug "----------------------------" 171 | If ($BackupSuccess -eq ($DoBackupDataDir + $DoBackupDB + $MiscBackupFiles.Count)) { 172 | Debug "All files backed up successfully" 173 | Email "[OK] Backed up data successfully" 174 | } Else { 175 | Debug "[ERROR] Backup count mismatch." 176 | Email "[ERROR] Backup count mismatch : Check Debug Log" 177 | } 178 | 179 | <# Restart SpamAssassin and hMailServer #> 180 | If ($UseSA) {ServiceStart $SAServiceName} 181 | ServiceStart $hMSServiceName 182 | Debug "----------------------------" 183 | Debug "hMailServer was out of service for $(ElapsedTime $BeginShutdownPeriod)" 184 | 185 | <# Prune hMailServer logs #> 186 | If ($PruneLogs) {PruneLogs} 187 | 188 | <# Prune backups #> 189 | If ($PruneBackups) {PruneBackups} 190 | 191 | <# Prune messages/empty folders older than N number of days #> 192 | If ($PruneMessages) {PruneMessages} 193 | 194 | <# Feed Beyesian database #> 195 | If ($UseSA) {If ($FeedBayes) {FeedBayes}} 196 | 197 | <# Compress backup into 7z archives #> 198 | If ($UseSevenZip) {MakeArchive} 199 | 200 | <# Check for updates #> 201 | CheckForUpdates 202 | 203 | <# Finish up and send email #> 204 | Debug "----------------------------" 205 | If (((Get-Item $DebugLog).length/1MB) -ge $MaxAttachmentSize) { 206 | Debug "Debug log larger than specified max attachment size. Do not attach to email message." 207 | Email "[INFO] Debug log larger than specified max attachment size. Log file stored in backup folder on server file system." 208 | } 209 | Debug "hMailServer Backup & Upload routine completed in $(ElapsedTime $StartScript)" 210 | Email " " 211 | Email "hMailServer Backup & Upload routine completed in $(ElapsedTime $StartScript)" 212 | If ($UseHTML) {Write-Output "
" | Out-File $EmailBody -Encoding ASCII -Append} 213 | EmailResults -------------------------------------------------------------------------------- /hMailServerBackupDownload.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .SYNOPSIS 4 | hMailServer Backup Download Utility 5 | 6 | .DESCRIPTION 7 | hMailServer Backup Download Utility 8 | 9 | .FUNCTIONALITY 10 | Looks for last backup at LetsUpload and downloads for restoration 11 | 12 | .PARAMETER 13 | 14 | 15 | .NOTES 16 | 17 | 18 | .EXAMPLE 19 | 20 | 21 | #> 22 | 23 | <### BEGIN SCRIPT ###> 24 | $StartScript = Get-Date 25 | 26 | <# Load required files #> 27 | Try { 28 | .("$PSScriptRoot\hMailServerBackupConfig.ps1") 29 | .("$PSScriptRoot\hMailServerBackupFunctions.ps1") 30 | } 31 | Catch { 32 | Write-Output "$(Get-Date) -f G) : ERROR : Unable to load supporting PowerShell Scripts" | out-file "$PSScriptRoot\PSError.log" -append 33 | Write-Output "$(Get-Date) -f G) : ERROR : $Error" | out-file "$PSScriptRoot\PSError.log" -append 34 | Exit 35 | } 36 | 37 | <# Clear out error variable #> 38 | $Error.Clear() 39 | 40 | <# Validate backup folder #> 41 | $BackupLocation = $BackupLocation -Replace('\\$','') 42 | ValidateFolders $BackupLocation 43 | $DownloadFolder = "$BackupLocation\hMailServer-Restoration" 44 | If (Test-Path $DownloadFolder) {Remove-Item -Force -Path $DownloadFolder -Recurse} 45 | New-Item -Path $DownloadFolder -ItemType Directory 46 | 47 | <# Delete old debug file and create new #> 48 | $EmailBody = "$PSScriptRoot\EmailBody.log" 49 | If (Test-Path $EmailBody) {Remove-Item -Force -Path $EmailBody} 50 | New-Item $EmailBody 51 | $DebugLog = "$BackupLocation\hMailServerDownloadRestore.log" 52 | If (Test-Path $DebugLog) {Remove-Item -Force -Path $DebugLog} 53 | New-Item $DebugLog 54 | Write-Output "::: hMailServer Download Offsite Backup Routine $(Get-Date -f D) :::" | Out-File $DebugLog -Encoding ASCII -Append 55 | Write-Output " " | Out-File $DebugLog -Encoding ASCII -Append 56 | If ($UseHTML) { 57 | Write-Output " 58 | 59 | 60 | 61 | hMailServer Backup & Offsite Upload 62 | 63 | 64 | 65 | 66 | " | Out-File $EmailBody -Encoding ASCII -Append 67 | } 68 | 69 | <# Authorize and get access token #> 70 | Debug "Getting access token from LetsUpload" 71 | $URIAuth = "https://letsupload.io/api/v2/authorize" 72 | $AuthBody = @{ 73 | 'key1' = $APIKey1; 74 | 'key2' = $APIKey2; 75 | } 76 | Try{ 77 | $Auth = Invoke-RestMethod -Method GET $URIAuth -Body $AuthBody -ContentType 'application/json; charset=utf-8' 78 | } 79 | Catch { 80 | Debug "[ERROR] LetsUpload Authentication : $Error" 81 | Email "[ERROR] LetsUpload Authentication : Check Debug Log" 82 | EmailResults 83 | Exit 84 | } 85 | $AccessToken = $Auth.data.access_token 86 | $AccountID = $Auth.data.account_id 87 | Debug "Access Token : $AccessToken" 88 | Debug "Account ID : $AccountID" 89 | 90 | <# Get folder_id of last upload #> 91 | $URIFolderListing = "https://letsupload.io/api/v2/folder/listing" 92 | $FLBody = @{ 93 | 'access_token' = $AccessToken; 94 | 'account_id' = $AccountID; 95 | 'parent_folder_id' = ""; 96 | } 97 | Try{ 98 | $FolderListing = Invoke-RestMethod -Method GET $URIFolderListing -Body $FLBody -ContentType 'application/json; charset=utf-8' 99 | } 100 | Catch { 101 | Debug "[ERROR] obtaining backup folder ID : $Error" 102 | Email "[ERROR] obtaining backup folder ID : Check Debug Log" 103 | EmailResults 104 | Exit 105 | } 106 | $NewestBackup = $FolderListing.data.folders | Sort-Object date_added -Descending | Where {$_.folderName -match "hMailServer"} | Select -First 1 107 | $FolderID = $NewestBackup.id 108 | $FolderName = $NewestBackup.folderName 109 | Debug "Folder Name: $FolderName" 110 | Debug "Folder ID : $FolderID" 111 | Email "[OK] Latest backup : $FolderName" 112 | 113 | <# Get file listing within latest backup folder #> 114 | $URIFolderListing = "https://letsupload.io/api/v2/folder/listing" 115 | $FLBody = @{ 116 | 'access_token' = $AccessToken; 117 | 'account_id' = $AccountID; 118 | 'parent_folder_id' = $FolderID; 119 | } 120 | Try{ 121 | $FileListing = Invoke-RestMethod -Method GET $URIFolderListing -Body $FLBody -ContentType 'application/json; charset=utf-8' 122 | } 123 | Catch { 124 | Debug "[ERROR] obtaining backup file listing : $Error" 125 | Email "[ERROR] obtaining backup file listing : Check Debug Log" 126 | EmailResults 127 | Exit 128 | } 129 | $DownloadCount = ($FileListing.data.files).Count 130 | $DownloadNumber = 1 131 | Debug "File count: $DownloadCount" 132 | Debug "Starting file download" 133 | Email "[OK] $DownloadCount files to download" 134 | 135 | <# Loop through results and download files #> 136 | $FileListing.data.files | ForEach { 137 | $FileID = $_.id 138 | $FileName = $_.filename 139 | $FileURL = $_.url_file 140 | $RemoteFileSize = $_.fileSize 141 | $RemoteFileSizeFormatted = "{0:N2}" -f (($RemoteFileSize)/1MB) 142 | Debug "----------------------------" 143 | Debug "File $DownloadNumber of $DownloadCount" 144 | Debug "File ID : $FileID" 145 | Debug "File Name : $FileName" 146 | Debug "File Size : $RemoteFileSizeFormatted mb" 147 | 148 | $URIDownload = "https://letsupload.io/api/v2/file/download" 149 | $DLBody = @{ 150 | 'access_token' = $AccessToken; 151 | 'account_id' = $AccountID; 152 | 'file_id' = $FileID; 153 | } 154 | 155 | <# Get download URL #> 156 | $GetURLTry = 1 157 | Do { 158 | $Error.Clear() 159 | Try{ 160 | $FileDownload = Invoke-RestMethod -Method GET $URIDownload -Body $DLBody -ContentType 'application/json; charset=utf-8' 161 | If ($FileDownload._status -match "success") { 162 | $URLSuccess = $True 163 | } Else { 164 | Throw "[ERROR] Getting download URL on Try $GetURLTry" 165 | } 166 | $DownloadURL = $FileDownload.data.download_url 167 | Debug "Download URL: $DownloadURL" 168 | } 169 | Catch { 170 | Debug "[ERROR] Getting download URL : $Error" 171 | $URLSuccess = $False 172 | } 173 | $GetURLTry++ 174 | } Until (($GetURLTry -eq 5) -or ($URLSuccess -eq $True)) 175 | 176 | <# If get download URL success, then download file #> 177 | If ($URLSuccess -eq $False) { 178 | Debug "[ERROR] obtaining download URL : Tried $GetURLTry times : Giving up" 179 | Debug "[ERROR] obtaining download URL : $Error" 180 | Email "[ERROR] obtaining download URL : Check Debug Log" 181 | EmailResults 182 | Exit 183 | } Else { 184 | <# Download file using BITS #> 185 | $DownloadTries = 1 186 | Do { 187 | $Error.Clear() 188 | Try { 189 | Debug "Download Try $DownloadTries" 190 | $BeginDL = Get-Date 191 | Import-Module BitsTransfer 192 | Start-BitsTransfer -Source $DownloadURL -Destination "$DownloadFolder\$FileName" 193 | $LocalFileSize = (Get-Item "$DownloadFolder\$FileName").Length 194 | If ($RemoteFileSize -ne $LocalFileSize) { 195 | Throw "[ERROR] Remote and local file sizes do not match" 196 | } 197 | Debug "File $DownloadNumber downloaded in $(ElapsedTime $BeginDL)" 198 | $DownloadFileSuccess = $True 199 | } 200 | Catch { 201 | Debug "[ERROR] BITS downloading file $DownloadNumber of $FileCount : $Error" 202 | Debug "[ERROR] Remote file size: $RemoteFileSize" 203 | Debug "[ERROR] Local file size : $LocalFileSize" 204 | $DownloadFileSuccess = $False 205 | } 206 | $DownloadTries++ 207 | } Until (($DownloadFileSuccess -eq $True) -or ($DownloadTries -eq 5)) 208 | If ($DownloadFileSuccess -eq $False) { 209 | Debug "[ERROR] Downloading File : Tried $GetURLTry times : Giving up" 210 | Debug "[ERROR] Downloading File : $Error" 211 | Email "[ERROR] Downloading File : Check Debug Log" 212 | EmailResults 213 | Exit 214 | } 215 | } 216 | $DownloadNumber++ 217 | } 218 | 219 | <# Count and compare remote to local files #> 220 | If ($DownloadCount -eq ((Get-ChildItem $DownloadFolder).Count)) { 221 | Debug "----------------------------" 222 | Debug "Download successful. $DownloadCount files downloaded to $DownloadFolder" 223 | Email "[OK] Download successful. $DownloadCount files downloaded to $DownloadFolder" 224 | } Else { 225 | Debug "[ERROR] Download unsuccessful : Remote and local file counts do not match!" 226 | Email "[ERROR] Download unsuccessful : Remote and local file counts do not match!" 227 | } 228 | 229 | <# Finish up and email results #> 230 | Debug "----------------------------" 231 | Debug "Script completed in $(ElapsedTime $StartScript)" 232 | Email " " 233 | Email "Script completed in $(ElapsedTime $StartScript)" 234 | Debug "Sending Email" 235 | If (($AttachDebugLog) -and (Test-Path $DebugLog) -and (((Get-Item $DebugLog).length/1MB) -gt $MaxAttachmentSize)){ 236 | Email "Debug log size exceeds maximum attachment size. Please see log file in script folder" 237 | } 238 | If ($UseHTML) {Write-Output "
" | Out-File $EmailBody -Encoding ASCII -Append} 239 | EmailResults -------------------------------------------------------------------------------- /hMailServerBackupConfig.dist.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .SYNOPSIS 4 | hMailServer Backup 5 | 6 | .DESCRIPTION 7 | Configuration for hMailServer Backup 8 | 9 | .FUNCTIONALITY 10 | Backs up hMailServer, compresses backup and uploads to LetsUpload.io 11 | 12 | .PARAMETER 13 | 14 | 15 | .NOTES 16 | Run at 11:58PM from task scheduler in order to properly cycle log files. 17 | 18 | .EXAMPLE 19 | 20 | 21 | #> 22 | 23 | <### USER VARIABLES ###> 24 | $VerboseConsole = $True # If true, will output debug to console 25 | $VerboseFile = $True # If true, will output debug to file 26 | 27 | <### DATA DIR BACKUP ###> 28 | $BackupDataDir = $True # If true, will backup data dir via robocopy 29 | 30 | <### MISCELLANEOUS BACKUP FILES ###> 31 | $BackupMisc = $True # True will backup misc files listed below 32 | $MiscBackupFiles = @( # Array of additional miscellaneous files to backup (use full path) 33 | "C:\hMailServer\Bin\hMailServer.INI" 34 | "C:\hMailServer\Events\EventHandlers.vbs" 35 | "C:\Program Files\JAM Software\SpamAssassin for Windows\etc\spamassassin\local.cf" 36 | ) 37 | 38 | <### FOLDER LOCATIONS ###> 39 | $hMSDir = "C:\hMailServer" # hMailServer Install Directory 40 | $SADir = "C:\Program Files\JAM Software\SpamAssassin for Windows" # SpamAssassin Install Directory 41 | $SAConfDir = "C:\Program Files\JAM Software\SpamAssassin for Windows\etc\spamassassin" # SpamAssassin Conf Directory 42 | $MailDataDir = "C:\HMS-DATA" # hMailServer Data Dir 43 | $BackupTempDir = "C:\HMS-BACKUP-TEMP" # Temporary backup folder for RoboCopy to compare 44 | $BackupLocation = "C:\HMS-BACKUP" # Location archive files will be stored 45 | $MySQLBINdir = "C:\xampp\mysql\bin" # MySQL BIN folder location 46 | 47 | <### HMAILSERVER COM VARIABLES ###> 48 | $hMSAdminPass = "supersecretpassword" # hMailServer Admin password 49 | 50 | <### SPAMASSASSIN VARIABLES ###> 51 | $UseSA = $True # Specifies whether SpamAssassin is in use 52 | $UseCustomRuleSets = $True # Specifies whether to download and update KAM.cf 53 | $SACustomRules = @( # URLs of custom rulesets 54 | "https://www.pccc.com/downloads/SpamAssassin/contrib/KAM.cf" 55 | "https://www.pccc.com/downloads/SpamAssassin/contrib/nonKAMrules.cf" 56 | ) 57 | 58 | <### OPENPHISH VARIABLES ###> # https://hmailserver.com/forum/viewtopic.php?t=40295 59 | $UseOpenPhish = $True # Specifies whether to update OpenPhish databases - for use with Phishing plugin for SA - requires wget in the system path 60 | $PhishFiles = @{ 61 | "https://data.phishtank.com/data/online-valid.csv" = "$SAConfDir\phishtank-feed.csv" 62 | "https://openphish.com/feed.txt" = "$SAConfDir\openphish-feed.txt" 63 | } 64 | # "https://phishstats.info/phish_score.csv" = "$SAConfDir\phishstats-feed.csv" #OpenPhish is dead. 65 | 66 | <### WINDOWS SERVICE VARIABLES ###> 67 | $hMSServiceName = "hMailServer" # Name of hMailServer Service (check windows services to verify exact spelling) 68 | $SAServiceName = "SpamAssassin" # Name of SpamAssassin Service (check windows services to verify exact spelling) 69 | $ServiceTimeout = 5 # number of minutes to continue trying if service start or stop commands become unresponsive 70 | 71 | <### PRUNE BACKUPS VARIABLES ###> 72 | $PruneBackups = $True # If true, will delete local backups older than N days 73 | $DaysToKeepBackups = 5 # Number of days to keep backups - older backups will be deleted 74 | 75 | <### PRUNE MESSAGES VARIABLES ###> 76 | $DoDelete = $True # FOR TESTING - set to FALSE to run and report results without deleting messages and folders 77 | $PruneMessages = $True # True will run message pruning routine 78 | $PruneSubFolders = $True # True will prune messages in folders levels below name matching folders 79 | $PruneEmptySubFolders = $True # True will delete empty subfolders below the matching level unless a subfolder within contains messages 80 | $DaysBeforeDelete = 30 # Number of days to keep messages in pruned folders 81 | $SkipAccountPruning = "user@dom.com|a@b.com" # User accounts to skip - uses regex (disable with "" or $NULL) 82 | $SkipDomainPruning = "domain.tld|dom2.com" # Domains to skip - uses regex (disable with "" or $NULL) 83 | $PruneFolders = "Trash|Deleted|Junk|Spam|Folder-[0-9]{6}|Unsubscribes" # Names of IMAP folders you want to cleanup - uses regex 84 | 85 | <### FEED BAYES VARIABLES ###> 86 | $FeedBayes = $True # True will run Bayes feeding routine 87 | $DoSpamC = $True # FOR TESTING - set to FALSE to run and report results without feeding SpamC with spam/ham 88 | $BayesSubFolders = $True # True will feed messages from subfolders within regex name matching folders 89 | $BayesDays = 7 # Number of days worth of spam/ham to feed to bayes 90 | $HamFolders = "INBOX|Ham" # Ham folders to feed messages to spamC for bayes database - uses regex 91 | $SpamFolders = "Spam|Junk" # Spam folders to feed messages to spamC for bayes database - uses regex 92 | $SkipAccountBayes = "user@dom.com|a@b.com" # User accounts to skip - uses regex (disable with "" or $NULL) 93 | $SkipDomainBayes = "domain.tld|dom2.com" # Domains to skip - uses regex (disable with "" or $NULL) 94 | $SyncBayesJournal = $True # True will sync bayes_journal after feeding messages to SpamC 95 | $BackupBayesDatabase = $True # True will backup the bayes database to bayes_backup - NOT insert the file in the backup/upload routine 96 | $BayesBackupLocation = "C:\bayes_backup" # Bayes backup FILE 97 | 98 | <### MySQL VARIABLES ###> 99 | $BackupDB = $True # Specifies whether to run BackupDatabases function (options below)(FALSE will skip) 100 | $UseMySQL = $True # Specifies whether database used is MySQL 101 | $BackupAllMySQLDatbase = $True # True will backup all databases, not just hmailserver - must use ROOT user for this 102 | $MySQLDatabase = "hmailserver" # MySQL database name 103 | $MySQLUser = "root" # hMailServer database user 104 | $MySQLPass = "supersecretpassword" # hMailServer database password 105 | $MySQLPort = 3306 # MySQL port 106 | 107 | <### 7-ZIP VARIABLES ###> 108 | $UseSevenZip = $True # True will compress backup files into archive 109 | $PWProtectedArchive = $True # False = no-password zip archive, True = AES-256 encrypted multi-volume 7z archive 110 | $VolumeSize = "100m" # Size of archive volume parts - maximum 200m recommended - valid suffixes for size units are (b|k|m|g) 111 | $ArchivePassword = "supersecretpassword" # Password to 7z archive 112 | 113 | <### HMAILSERVER LOG VARIABLES ###> 114 | $PruneLogs = $True # If true, will delete logs in hMailServer \Logs folder older than N days 115 | $DaysToKeepLogs = 10 # Number of days to keep old hMailServer Logs 116 | 117 | <### CYCLE LOGS VARIABLES ###> # Array of logs to cycle - Full file path required - not limited to hmailserver log dir 118 | $CycleLogs = $True # True will cycle logs (rename with today's date) 119 | $LogsToCycle = @( 120 | "C:\hMailServer\Logs\hmailserver_events.log" 121 | "C:\hMailServer\Logs\spamd.log" 122 | ) 123 | 124 | <### EMAIL VARIABLES ###> 125 | $EmailFrom = "notify@mydomain.tld" 126 | $EmailTo = "admin@mydomain.tld" 127 | $Subject = "hMailServer Nightly Backup" 128 | $SMTPServer = "mail.mydomain.tld" 129 | $SMTPAuthUser = "notify@mydomain.tld" 130 | $SMTPAuthPass = "supersecretpassword" 131 | $SMTPPort = 587 132 | $SSL = $True # If true, will use tls connection to send email 133 | $UseHTML = $True # If true, will format and send email body as html (with color!) 134 | $AttachDebugLog = $True # If true, will attach debug log to email report - must also select $VerboseFile 135 | $MaxAttachmentSize = 1 # Size in MB 136 | 137 | <### GMAIL VARIABLES ###> 138 | <# Alternate messaging in case of hMailServer failure #> 139 | <# "Less Secure Apps" must be enabled in gmail account settings #> 140 | $GmailUser = "notifier@gmail.com" 141 | $GmailPass = "supersecretpassword" 142 | $GmailTo = "1234567890@tmomail.net" -------------------------------------------------------------------------------- /hMailServerBackupPruneMessagesTEST.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .SYNOPSIS 4 | Prune Messages 5 | 6 | .DESCRIPTION 7 | Delete messages in specified folders older than N days 8 | 9 | .FUNCTIONALITY 10 | Looks for folder name match at any folder level and if found, deletes all messages older than N days within that folder and all subfolders within 11 | Deletes empty subfolders within matching folders if DeleteEmptySubFolders set to True in config 12 | 13 | .PARAMETER 14 | 15 | 16 | .NOTES 17 | Folder name matching occurs at any level folder 18 | Empty folders are assumed to be trash if they're located in this script 19 | Only empty folders found in levels BELOW matching level will be deleted 20 | 21 | .EXAMPLE 22 | 23 | 24 | #> 25 | 26 | <### USER VARIABLES ###> 27 | $hMSAdminPass = "secretpassword" # hMailServer Admin password 28 | $DoDelete = $False # FOR TESTING - set to false to run and report results without deleting messages and folders 29 | $PruneSubFolders = $True # True will prune all folders in levels below name matching folders 30 | $DeleteEmptySubFolders = $True # True will delete empty subfolders below the matching level unless a subfolder within contains messages 31 | $DaysBeforeDelete = 30 # Number of days to keep messages in pruned folders 32 | $PruneFolders = "2nd level test|2020-[0-1][0-9]-[0-3][0-9]$|Trash|Deleted|Junk|Spam|Unsubscribes" # Names of IMAP folders you want to cleanup - uses regex 33 | 34 | <# Functions copied from hMailServer Backup required for testing #> 35 | Function Debug ($DebugOutput) {Write-Host $DebugOutput} 36 | Function Email ($DebugOutput) {} 37 | Function ElapsedTime ($EndTime) { 38 | $TimeSpan = New-Timespan $EndTime 39 | If (([int]($TimeSpan).Hours) -eq 0) {$Hours = ""} ElseIf (([int]($TimeSpan).Hours) -eq 1) {$Hours = "1 hour "} Else {$Hours = "$([int]($TimeSpan).Hours) hours "} 40 | If (([int]($TimeSpan).Minutes) -eq 0) {$Minutes = ""} ElseIf (([int]($TimeSpan).Minutes) -eq 1) {$Minutes = "1 minute "} Else {$Minutes = "$([int]($TimeSpan).Minutes) minutes "} 41 | If (([int]($TimeSpan).Seconds) -eq 1) {$Seconds = "1 second"} Else {$Seconds = "$([int]($TimeSpan).Seconds) seconds"} 42 | If (($TimeSpan).TotalSeconds -lt 1) { 43 | $Return = "less than 1 second" 44 | } Else { 45 | $Return = "$Hours$Minutes$Seconds" 46 | } 47 | Return $Return 48 | } 49 | Function Plural ($Integer) { 50 | If ($Integer -eq 1) {$S = ""} Else {$S = "s"} 51 | Return $S 52 | } 53 | 54 | <# Begin hMailServerBackupPruneMessages #> 55 | 56 | Set-Variable -Name TotalDeletedMessages -Value 0 -Option AllScope 57 | Set-Variable -Name TotalDeletedFolders -Value 0 -Option AllScope 58 | Set-Variable -Name DeleteMessageErrors -Value 0 -Option AllScope 59 | Set-Variable -Name DeleteFolderErrors -Value 0 -Option AllScope 60 | 61 | Function GetSubFolders ($Folder) { 62 | $IterateFolder = 0 63 | $ArrayDeletedFolders = @() 64 | If ($Folder.SubFolders.Count -gt 0) { 65 | Do { 66 | $SubFolder = $Folder.SubFolders.Item($IterateFolder) 67 | $SubFolderName = $SubFolder.Name 68 | $SubFolderID = $SubFolder.ID 69 | If ($SubFolder.Subfolders.Count -gt 0) {GetSubFolders $SubFolder} 70 | If ($SubFolder.Messages.Count -gt 0) { 71 | If ($PruneSubFolders) {GetMessages $SubFolder} 72 | } Else { 73 | If ($DeleteEmptySubFolders) {$ArrayDeletedFolders += $SubFolderID} 74 | } 75 | $IterateFolder++ 76 | } Until ($IterateFolder -eq $Folder.SubFolders.Count) 77 | } 78 | If ($DeleteEmptySubFolders) { 79 | $ArrayDeletedFolders | ForEach { 80 | $CheckFolder = $Folder.SubFolders.ItemByDBID($_) 81 | $FolderName = $CheckFolder.Name 82 | If (SubFoldersEmpty $CheckFolder) { 83 | Try { 84 | If ($DoDelete) {$Folder.SubFolders.DeleteByDBID($_)} 85 | $TotalDeletedFolders++ 86 | Debug "Deleted empty subfolder $FolderName in $AccountAddress" 87 | } 88 | Catch { 89 | $DeleteFolderErrors++ 90 | Debug "[ERROR] Deleting empty subfolder $FolderName in $AccountAddress" 91 | Debug "[ERROR] : $Error" 92 | } 93 | $Error.Clear() 94 | } 95 | } 96 | } 97 | $ArrayDeletedFolders.Clear() 98 | } 99 | 100 | Function SubFoldersEmpty ($Folder) { 101 | $IterateFolder = 0 102 | If ($Folder.SubFolders.Count -gt 0) { 103 | Do { 104 | $SubFolder = $Folder.SubFolders.Item($IterateFolder) 105 | If ($SubFolder.Messages.Count -gt 0) { 106 | Return $False 107 | Break 108 | } 109 | If ($SubFolder.SubFolders.Count -gt 0) { 110 | SubFoldersEmpty $SubFolder 111 | } 112 | $IterateFolder++ 113 | } Until ($IterateFolder -eq $Folder.SubFolders.Count) 114 | } Else { 115 | Return $True 116 | } 117 | } 118 | 119 | Function GetMatchFolders ($Folder) { 120 | $IterateFolder = 0 121 | If ($Folder.SubFolders.Count -gt 0) { 122 | Do { 123 | $SubFolder = $Folder.SubFolders.Item($IterateFolder) 124 | $SubFolderName = $SubFolder.Name 125 | If ($SubFolderName -match [regex]$PruneFolders) { 126 | GetSubFolders $SubFolder 127 | GetMessages $SubFolder 128 | } Else { 129 | GetMatchFolders $SubFolder 130 | } 131 | $IterateFolder++ 132 | } Until ($IterateFolder -eq $Folder.SubFolders.Count) 133 | } 134 | } 135 | 136 | Function GetMessages ($Folder) { 137 | $IterateMessage = 0 138 | $ArrayMessagesToDelete = @() 139 | $DeletedMessages = 0 140 | If ($Folder.Messages.Count -gt 0) { 141 | Do { 142 | $Message = $Folder.Messages.Item($IterateMessage) 143 | If ($Message.InternalDate -lt ((Get-Date).AddDays(-$DaysBeforeDelete))) { 144 | $ArrayMessagesToDelete += $Message.ID 145 | } 146 | $IterateMessage++ 147 | } Until ($IterateMessage -eq $Folder.Messages.Count) 148 | } 149 | $ArrayMessagesToDelete | ForEach { 150 | $AFolderName = $Folder.Name 151 | Try { 152 | If ($DoDelete) {$Folder.Messages.DeleteByDBID($_)} 153 | $DeletedMessages++ 154 | $TotalDeletedMessages++ 155 | } 156 | Catch { 157 | $DeleteMessageErrors++ 158 | Debug "[ERROR] Deleting messages from folder $AFolderName in $AccountAddress" 159 | Debug "[ERROR] $Error" 160 | } 161 | $Error.Clear() 162 | } 163 | If ($DeletedMessages -gt 0) { 164 | Debug "Deleted $DeletedMessages message$(Plural $DeletedMessages) from $AFolderName in $AccountAddress" 165 | } 166 | $ArrayMessagesToDelete.Clear() 167 | } 168 | 169 | Function PruneMessages { 170 | 171 | $Error.Clear() 172 | $BeginDeletingOldMessages = Get-Date 173 | Debug "----------------------------" 174 | Debug "Begin deleting messages older than $DaysBeforeDelete days" 175 | If (-not($DoDelete)) { 176 | Debug "Delete disabled - Test Run ONLY" 177 | } 178 | 179 | <# Authenticate hMailServer COM #> 180 | $hMS = New-Object -COMObject hMailServer.Application 181 | $hMS.Authenticate("Administrator", $hMSAdminPass) | Out-Null 182 | 183 | $IterateDomains = 0 184 | Do { 185 | $hMSDomain = $hMS.Domains.Item($IterateDomains) 186 | If ($hMSDomain.Active) { 187 | $IterateAccounts = 0 188 | Do { 189 | $hMSAccount = $hMSDomain.Accounts.Item($IterateAccounts) 190 | If ($hMSAccount.Active) { 191 | $AccountAddress = $hMSAccount.Address 192 | $IterateIMAPFolders = 0 193 | If ($hMSAccount.IMAPFolders.Count -gt 0) { 194 | Do { 195 | $hMSIMAPFolder = $hMSAccount.IMAPFolders.Item($IterateIMAPFolders) 196 | If ($hMSIMAPFolder.Name -match [regex]$PruneFolders) { 197 | If ($hMSIMAPFolder.SubFolders.Count -gt 0) { 198 | GetSubFolders $hMSIMAPFolder 199 | } # IF SUBFOLDER COUNT > 0 200 | GetMessages $hMSIMAPFolder 201 | } # IF FOLDERNAME MATCH REGEX 202 | Else { 203 | GetMatchFolders $hMSIMAPFolder 204 | } # IF NOT FOLDERNAME MATCH REGEX 205 | $IterateIMAPFolders++ 206 | } Until ($IterateIMAPFolders -eq $hMSAccount.IMAPFolders.Count) 207 | } # IF IMAPFOLDER COUNT > 0 208 | } #IF ACCOUNT ACTIVE 209 | $IterateAccounts++ 210 | } Until ($IterateAccounts -eq $hMSDomain.Accounts.Count) 211 | } # IF DOMAIN ACTIVE 212 | $IterateDomains++ 213 | } Until ($IterateDomains -eq $hMS.Domains.Count) 214 | 215 | If ($DeleteMessageErrors -gt 0) { 216 | Debug "Finished Message Pruning : $DeleteMessageErrors Errors present" 217 | Email "[ERROR] Message Pruning : $DeleteMessageErrors Errors present : Check debug log" 218 | } Else { 219 | If ($TotalDeletedMessages -gt 0) { 220 | Debug "Finished pruning $TotalDeletedMessages message$(Plural $TotalDeletedMessages) in $(ElapsedTime $BeginDeletingOldMessages)" 221 | Email "[OK] Finished pruning $TotalDeletedMessages message$(Plural $TotalDeletedMessages) in $(ElapsedTime $BeginDeletingOldMessages)" 222 | } Else { 223 | Debug "No messages older than $DaysBeforeDelete days to prune" 224 | Email "[OK] No messages older than $DaysBeforeDelete days to prune" 225 | } 226 | } 227 | If ($DeleteFolderErrors -gt 0) { 228 | Debug "Deleting Empty Folders : $DeleteFolderErrors Error$(Plural $DeleteFolderErrors) present" 229 | Email "[ERROR] Deleting Empty Folders : $DeleteFolderErrors Error$(Plural $DeleteFolderErrors) present : Check debug log" 230 | } Else { 231 | If ($TotalDeletedFolders -gt 0) { 232 | Debug "Finished deleting $TotalDeletedFolders empty subfolder$(Plural $TotalDeletedFolders)" 233 | Email "[OK] Deleted $TotalDeletedFolders empty subfolder$(Plural $TotalDeletedFolders)" 234 | } Else { 235 | Debug "No empty subfolders deleted" 236 | Email "[OK] No empty subfolders deleted" 237 | } 238 | } 239 | } 240 | 241 | PruneMessages -------------------------------------------------------------------------------- /hMailServerBackupFunctions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .SYNOPSIS 4 | hMailServer Backup 5 | 6 | .DESCRIPTION 7 | Common Code for hMailServer Backup 8 | 9 | .FUNCTIONALITY 10 | Backs up hMailServer, compresses backup and uploads to LetsUpload 11 | 12 | .PARAMETER 13 | 14 | 15 | .NOTES 16 | Run at 11:58PM from task scheduler in order to properly cycle log files. 17 | 18 | .EXAMPLE 19 | 20 | 21 | #> 22 | 23 | <# Miscellaneous Functions #> 24 | 25 | Function Debug ($DebugOutput) { 26 | If ($VerboseFile) {Write-Output "$(Get-Date -f G) : $DebugOutput" | Out-File $DebugLog -Encoding ASCII -Append} 27 | If ($VerboseConsole) {Write-Host "$(Get-Date -f G) : $DebugOutput"} 28 | } 29 | 30 | Function Email ($EmailOutput) { 31 | If ($UseHTML){ 32 | If ($EmailOutput -match "\[OK\]") {$EmailOutput = $EmailOutput -Replace "\[OK\]","[OK]"} 33 | If ($EmailOutput -match "\[INFO\]") {$EmailOutput = $EmailOutput -Replace "\[INFO\]","[INFO]"} 34 | If ($EmailOutput -match "\[ERROR\]") {$EmailOutput = $EmailOutput -Replace "\[ERROR\]","[ERROR]"} 35 | If ($EmailOutput -match "^\s$") {$EmailOutput = $EmailOutput -Replace "\s"," "} 36 | Write-Output "$EmailOutput" | Out-File $EmailBody -Encoding ASCII -Append 37 | } Else { 38 | Write-Output $EmailOutput | Out-File $EmailBody -Encoding ASCII -Append 39 | } 40 | } 41 | 42 | Function Plural ($Integer) { 43 | If ($Integer -eq 1) {$S = ""} Else {$S = "s"} 44 | Return $S 45 | } 46 | 47 | Function EmailResults { 48 | Try { 49 | $Body = (Get-Content -Path $EmailBody | Out-String ) 50 | If (($AttachDebugLog) -and (Test-Path $DebugLog) -and (((Get-Item $DebugLog).length/1MB) -lt $MaxAttachmentSize)){$Attachment = New-Object System.Net.Mail.Attachment $DebugLog} 51 | $Message = New-Object System.Net.Mail.Mailmessage $EmailFrom, $EmailTo, $Subject, $Body 52 | $Message.IsBodyHTML = $UseHTML 53 | If (($AttachDebugLog) -and (Test-Path $DebugLog) -and (((Get-Item $DebugLog).length/1MB) -lt $MaxAttachmentSize)){$Message.Attachments.Add($DebugLog)} 54 | $SMTP = New-Object System.Net.Mail.SMTPClient $SMTPServer,$SMTPPort 55 | $SMTP.EnableSsl = $SSL 56 | $SMTP.Credentials = New-Object System.Net.NetworkCredential($SMTPAuthUser, $SMTPAuthPass); 57 | $SMTP.Send($Message) 58 | } 59 | Catch { 60 | Debug "Email ERROR : $($Error[0])" 61 | } 62 | } 63 | 64 | Function GmailResults ($GBody){ 65 | Try { 66 | $Subject = "hMailServer Backup Problem" 67 | $Message = New-Object System.Net.Mail.Mailmessage $GmailUser, $GmailTo, $Subject, $GBody 68 | $Message.IsBodyHTML = $False 69 | $SMTP = New-Object System.Net.Mail.SMTPClient("smtp.gmail.com", 587) 70 | $SMTP.EnableSsl = $True 71 | $SMTP.Credentials = New-Object System.Net.NetworkCredential($GmailUser, $GmailPass); 72 | $SMTP.Send($Message) 73 | } 74 | Catch { 75 | Debug "Gmail ERROR : $($Error[0])" 76 | } 77 | } 78 | 79 | Function ValidateFolders ($Folder) { 80 | If (-not(Test-Path $Folder)) { 81 | Debug "[ERROR] Folder location $Folder does not exist : Quitting script" 82 | Email "[ERROR] Folder location $Folder does not exist : Quitting script" 83 | EmailResults 84 | Exit 85 | } 86 | } 87 | 88 | Function ElapsedTime ($EndTime) { 89 | $TimeSpan = New-Timespan $EndTime 90 | If (([math]::Floor(($TimeSpan).Days)) -eq 0) {$Days = ""} ElseIf (([math]::Floor(($TimeSpan).Days)) -eq 1) {$Days = "1 day "} Else {$Days = "$([math]::Floor(($TimeSpan).Days)) days "} 91 | If (([math]::Floor(($TimeSpan).Hours)) -eq 0) {$Hours = ""} ElseIf (([math]::Floor(($TimeSpan).Hours)) -eq 1) {$Hours = "1 hour "} Else {$Hours = "$([math]::Floor(($TimeSpan).Hours)) hours "} 92 | If (([math]::Floor(($TimeSpan).Minutes)) -eq 0) {$Minutes = ""} ElseIf (([math]::Floor(($TimeSpan).Minutes)) -eq 1) {$Minutes = "1 minute "} Else {$Minutes = "$([math]::Floor(($TimeSpan).Minutes)) minutes "} 93 | If (([math]::Floor(($TimeSpan).Seconds)) -eq 1) {$Seconds = "1 second"} Else {$Seconds = "$([math]::Floor(($TimeSpan).Seconds)) seconds"} 94 | 95 | If (($TimeSpan).TotalSeconds -lt 1) { 96 | $Return = "less than 1 second" 97 | } Else { 98 | $Return = "$Days$Hours$Minutes$Seconds" 99 | } 100 | Return $Return 101 | } 102 | 103 | <# Service start and stop functions #> 104 | Function ServiceStop ($ServiceName) { 105 | <# Check to see if already stopped #> 106 | $BeginShutdownRoutine = Get-Date 107 | Debug "----------------------------" 108 | Debug "Stop $ServiceName" 109 | $ServiceRunning = $False 110 | (Get-Service $ServiceName).Refresh() 111 | If ((Get-Service $ServiceName).Status -eq 'Stopped'){ 112 | Debug "$ServiceName already STOPPED. Nothing to stop. Check event logs." 113 | Email "[INFO] $ServiceName : service already STOPPED. Check event logs." 114 | } Else { 115 | Debug "$ServiceName running. Preparing to stop service." 116 | $ServiceRunning = $True 117 | } 118 | 119 | <# Stop service routine #> 120 | If ($ServiceRunning) { 121 | Debug "$ServiceName shutting down." 122 | $BeginShutdown = Get-Date 123 | Do { 124 | Stop-Service $ServiceName 125 | # Start-Sleep -Seconds 5 126 | (Get-Service $ServiceName).Refresh() 127 | $ServiceStatus = (Get-Service $ServiceName).Status 128 | } Until (((New-Timespan $BeginShutdown).TotalMinutes -gt $ServiceTimeout) -or ($ServiceStatus -eq "Stopped")) 129 | 130 | If ($ServiceStatus -ne "Stopped"){ 131 | Debug "$ServiceName failed to stop." 132 | GmailResults "$ServiceName failed to stop during backup routine! Check status NOW!" 133 | Break 134 | } Else { 135 | Debug "$ServiceName successfully stopped in $(ElapsedTime $BeginShutdownRoutine)" 136 | Email "[OK] $ServiceName stopped" 137 | } 138 | } 139 | } 140 | 141 | Function ServiceStart ($ServiceName) { 142 | <# Check to see if already running #> 143 | $BeginStartupRoutine = Get-Date 144 | Debug "----------------------------" 145 | Debug "Start $ServiceName" 146 | $ServiceStopped = $False 147 | (Get-Service $ServiceName).Refresh() 148 | If ((Get-Service $ServiceName).Status -eq 'Running'){ 149 | Debug "$ServiceName already RUNNING. Nothing to start." 150 | Email "[INFO] $ServiceName : service already RUNNING. Check event logs." 151 | } Else { 152 | Debug "$ServiceName not running. Preparing to start service." 153 | $ServiceStopped = $True 154 | } 155 | 156 | <# Start service routine #> 157 | If ($ServiceStopped) { 158 | Debug "$ServiceName starting up" 159 | $BeginStartup = Get-Date 160 | Do { 161 | Start-Service $ServiceName 162 | # Start-Sleep -Seconds 5 163 | (Get-Service $ServiceName).Refresh() 164 | $ServiceStatus = (Get-Service $ServiceName).Status 165 | } Until (((New-Timespan $BeginStartup).TotalMinutes -gt $ServiceTimeout) -or ($ServiceStatus -eq "Running")) 166 | 167 | If ($ServiceStatus -ne "Running"){ 168 | Debug "$ServiceName failed to start" 169 | GmailResults "$ServiceName failed to start during backup routine! Check status NOW!" 170 | Break 171 | } Else { 172 | Debug "$ServiceName successfully started in $(ElapsedTime $BeginStartupRoutine)" 173 | Email "[OK] $ServiceName started" 174 | } 175 | } 176 | } 177 | 178 | <# Update SpamAssassin Function #> 179 | Function UpdateSpamassassin { 180 | Debug "----------------------------" 181 | Debug "Updating SpamAssassin" 182 | $BeginSAUpdate = Get-Date 183 | $SAUD = "$SADir\sa-update.exe" 184 | Try { 185 | $SAUpdate = & $SAUD -v --nogpg --channel updates.spamassassin.org | Out-String 186 | Debug $SAUpdate 187 | Debug "Finished updating SpamAssassin in $(ElapsedTime $BeginSAUpdate)" 188 | Email "[OK] SpamAssassin updated" 189 | If ($SAUpdate -match "Update finished, no fresh updates were available"){ 190 | Email "[INFO] No fresh updates available" 191 | } 192 | } 193 | Catch { 194 | Debug "[ERROR] SpamAssassin update : $($Error[0])" 195 | Email "[ERROR] SpamAssassin update : Check Debug Log" 196 | } 197 | } 198 | 199 | <# Update custom rulesets function #> 200 | Function UpdateCustomRulesets { 201 | Debug "----------------------------" 202 | Debug "Updating custom Spamassassin rule sets" 203 | $CustomRuleSuccess = 0 204 | $CustomRuleSetCount = $SACustomRules.Count 205 | $BeginUpdatingCustomRuleSets = Get-Date 206 | $SACustomRules | ForEach { 207 | $CustomRuleURI = $_ 208 | $URLFileName = $_.Substring($_.LastIndexOf("/") + 1) 209 | $CustomRuleOutput = "$SAConfDir\$URLFileName" 210 | Try { 211 | Start-BitsTransfer -Source $CustomRuleURI -Destination $CustomRuleOutput -ErrorAction Stop 212 | Debug "$URLFileName update successful" 213 | $CustomRuleSuccess++ 214 | } 215 | Catch { 216 | Debug "[ERROR] : Unable to update $URLFileName : $($Error[0])" 217 | } 218 | } 219 | If ($CustomRuleSuccess -eq $CustomRuleSetCount) { 220 | Debug "All $CustomRuleSetCount custom rule sets updated successfully in $(ElapsedTime $BeginUpdatingCustomRuleSets)" 221 | Email "[OK] $CustomRuleSetCount Custom rules updated" 222 | } Else { 223 | Email "[ERROR] : Custom ruleset update unsuccessful : Check debug log" 224 | } 225 | } 226 | 227 | <# Update OpenPhish database files #> 228 | Function DownloadPhishFiles { 229 | Debug "----------------------------" 230 | Debug "Start downloading OpenPhish databases" 231 | $BeginOpenPhish = Get-Date 232 | $PhishUpdateSuccess = 0 233 | ForEach($Source in $PhishFiles.Keys){ 234 | $FileName = $(([System.IO.DirectoryInfo]$PhishFiles[$Source]).Name) 235 | $TempFile = "$BackupTempDir\$FileName" 236 | Try { 237 | & cmd /C wget --tries=3 -q -U "phishtank/downloader" -O $TempFile $Source 238 | If ($LastExitCode -ne 0) {Throw "[ERROR] Error downloading file $Source"} 239 | Debug "Successfully downloaded file $Source" 240 | Copy-Item $TempFile -Destination $PhishFiles[$Source] -Force 241 | Remove-Item $TempFile -Force 242 | $PhishUpdateSuccess++ 243 | } 244 | Catch { 245 | Debug $($Error[0]) 246 | Switch ($LastExitCode) { 247 | 0{Debug "[ERROR] No problems occurred."} 248 | 1{Debug "[ERROR] Generic error code."} 249 | 2{Debug "[ERROR] Parse error---for instance, when parsing command-line options, the .wgetrc or .netrc..."} 250 | 3{Debug "[ERROR] File I/O error."} 251 | 4{Debug "[ERROR] Network failure."} 252 | 5{Debug "[ERROR] SSL verification failure."} 253 | 6{Debug "[ERROR] Username/password authentication failure."} 254 | 7{Debug "[ERROR] Protocol errors."} 255 | 8{Debug "[ERROR] Server issued an error response."} 256 | } 257 | } 258 | } 259 | <# Report Misc Backup Success #> 260 | If ($PhishUpdateSuccess -eq $PhishFiles.Count) { 261 | Debug "All $($PhishFiles.Count) OpenPhish files successfully downloaded in $(ElapsedTime $BeginOpenPhish)" 262 | Email "[OK] $($PhishFiles.Count) OpenPhish files updated" 263 | } Else { 264 | Debug "[ERROR] Failed to download $($PhishFiles.Count - $PhishUpdateSuccess) of $($PhishFiles.Count) OpenPhish files" 265 | Debug "Finished updating OpenPhish files in $(ElapsedTime $BeginOpenPhish)" 266 | Email "[ERROR] $($PhishFiles.Count - $PhishUpdateSuccess) OpenPhish files failed to download : Check debug log" 267 | } 268 | } 269 | 270 | <# Backup hMailServer data dir function #> 271 | Function BackuphMailDataDir { 272 | $DoBackupDataDir++ 273 | Debug "----------------------------" 274 | Debug "Start backing up datadir with RoboCopy" 275 | $BeginRobocopy = Get-Date 276 | Try { 277 | $RoboCopy = & robocopy $MailDataDir "$BackupTempDir\hMailData" /mir /ndl /r:43200 /np /w:1 | Out-String 278 | Debug $RoboCopy 279 | Debug "Finished backing up data dir in $(ElapsedTime $BeginRobocopy)" 280 | $RoboStats = $RoboCopy.Split([Environment]::NewLine) | Where-Object {$_ -match 'Files\s:\s+\d'} 281 | $RoboStats | ConvertFrom-String -Delimiter "\s+" -PropertyNames Nothing, Files, Colon, Total, Copied, Skipped, Mismatch, Failed, Extras | ForEach { 282 | $Copied = $_.Copied 283 | $Mismatch = $_.Mismatch 284 | $Failed = $_.Failed 285 | $Extras = $_.Extras 286 | } 287 | If (($Mismatch -gt 0) -or ($Failed -gt 0)) { 288 | Throw "Robocopy MISMATCH or FAILED exists" 289 | } 290 | $BackupSuccess++ 291 | Debug "Robocopy backup success: $Copied new, $Extras deleted, $Mismatch mismatched, $Failed failed" 292 | Email "[OK] DataDir backed up: $Copied new, $Extras del" 293 | } 294 | Catch { 295 | Debug "[ERROR] DataDir RoboCopy : $($Error[0])" 296 | Email "[ERROR] DataDir RoboCopy : Check Debug Log" 297 | } 298 | } 299 | 300 | <# Backup database function #> 301 | Function BackupDatabases { 302 | $BeginDBBackup = Get-Date 303 | If ($UseMySQL) { 304 | $Error.Clear() 305 | $DoBackupDB++ 306 | Debug "----------------------------" 307 | Debug "Begin backing up MySQL" 308 | If (Test-Path "$BackupTempDir\hMailData\MYSQLDump_*.sql") { 309 | Get-ChildItem "$BackupTempDir\hMailData" | Where {$_.Extension -match "sql"} | ForEach { 310 | $OldMySQLDump = $_.Name 311 | Debug "Deleting old MySQL database dump : $OldMySQLDump" 312 | Try { 313 | Remove-Item "$BackupTempDir\hMailData\$OldMySQLDump" -ErrorAction Stop 314 | Debug "$OldMySQLDump database successfully deleted" 315 | } 316 | Catch { 317 | Debug "[ERROR] Old MySQL database delete : $($Error[0])" 318 | Email "[ERROR] Old MySQL database delete : Check Debug Log" 319 | } 320 | } 321 | } 322 | $MySQLDump = "$MySQLBINdir\mysqldump.exe" 323 | $MySQLDumpPass = "-p$MySQLPass" 324 | [System.IO.FileInfo]$MySQLDumpFile = "$BackupTempDir\hMailData\MYSQLDump_$((Get-Date).ToString('yyyy-MM-dd')).sql" 325 | Try { 326 | If ($BackupAllMySQLDatbase) { 327 | & $MySQLDump -u $MySQLUser $MySQLDumpPass --all-databases --result-file=$MySQLDumpFile 328 | } Else { 329 | & $MySQLDump -u $MySQLUser $MySQLDumpPass $MySQLDatabase --result-file=$MySQLDumpFile 330 | } 331 | $BackupSuccess++ 332 | Debug "MySQL successfully dumped $($MySQLDumpFile.Name) in $(ElapsedTime $BeginDBBackup)" 333 | } 334 | Catch { 335 | Debug "[ERROR] MySQL Dump : $($Error[0])" 336 | Email "[ERROR] MySQL Dump : Check Debug Log" 337 | } 338 | } Else { 339 | Debug "----------------------------" 340 | Debug "Begin backing up internal database" 341 | Debug "Copy internal database to backup folder" 342 | Try { 343 | $RoboCopyIDB = & robocopy "$hMSDir\Database" "$BackupTempDir\hMailData\Database" /mir /ndl /r:43200 /np /w:1 | Out-String 344 | $BackupSuccess++ 345 | Debug $RoboCopyIDB 346 | Debug "Internal DB successfully backed up in $(ElapsedTime $BeginDBBackup)" 347 | } 348 | Catch { 349 | Debug "[ERROR] RoboCopy Internal DB : $($Error[0])" 350 | Email "[ERROR] RoboCopy Internal DB : Check Debug Log" 351 | } 352 | } 353 | } 354 | 355 | <# Backup misc files function #> 356 | Function BackupMiscellaneousFiles { 357 | Debug "----------------------------" 358 | Debug "Begin backing up miscellaneous files" 359 | $MiscBackupSuccess = 0 360 | $MiscBackupFiles | ForEach { 361 | $MBUF = $_ 362 | $MBUFName = Split-Path -Path $MBUF -Leaf 363 | If (Test-Path "$BackupTempDir\hMailData\$MBUFName") { 364 | Try { 365 | Remove-Item -Force -Path "$BackupTempDir\hMailData\$MBUFName" 366 | # Debug "Previously backed up $MBUFName successfully deleted" 367 | } 368 | Catch { 369 | Debug "[ERROR] Could not delete previously backed up $MBUFName. Check Debug log for more info." 370 | } 371 | } 372 | If (Test-Path $MBUF) { 373 | Try { 374 | Copy-Item -Path $MBUF -Destination "$BackupTempDir\hMailData" 375 | $BackupSuccess++ 376 | $MiscBackupSuccess++ 377 | Debug "$MBUFName successfully backed up" 378 | } 379 | Catch { 380 | Debug "[ERROR] $MBUF Backup : $($Error[0])" 381 | Email "[ERROR] Backup $MBUFName : Check Debug Log" 382 | } 383 | } Else { 384 | Debug "$MBUF copy ERROR : File path not validated" 385 | } 386 | } 387 | 388 | <# Report Misc Backup Success #> 389 | If ($MiscBackupSuccess -eq $MiscBackupFiles.Count) { 390 | Debug "All $($MiscBackupFiles.Count) misc files backed up" 391 | } Else { 392 | Debug "[ERROR] Failed to backup $($MiscBackupFiles.Count - $MiscBackupSuccess) of $($MiscBackupFiles.Count) misc files" 393 | } 394 | } 395 | 396 | <# 7-zip archive creation function #> 397 | Function MakeArchive { 398 | $StartArchive = Get-Date 399 | Debug "----------------------------" 400 | Debug "Create archive : $BackupName" 401 | Debug "Archive folder : $BackupTempDir" 402 | $SevenZipExe = "$hMSDir\Bin\7za.exe" 403 | $VolumeSwitch = "-v$VolumeSize" 404 | $PWSwitch = "-p$ArchivePassword" 405 | Try { 406 | If ($PWProtectedArchive) { 407 | $SevenZip = & cmd /c $SevenZipExe a $VolumeSwitch -t7z -m0=lzma2 -mx=9 -mfb=64 -md=32m -ms=on -mhe=on $PWSwitch "$BackupLocation\$BackupName\$BackupName.7z" "$BackupTempDir\*" | Out-String 408 | } Else { 409 | $SevenZip = & cmd /c $SevenZipExe a -tzip "$BackupLocation\$BackupName\$BackupName.zip" "$BackupTempDir\*" | Out-String 410 | } 411 | Debug $SevenZip 412 | Debug "Archive creation finished in $(ElapsedTime $StartArchive)" 413 | Debug "Wait a few seconds to make sure archive is finished" 414 | Email "[OK] 7z archive created" 415 | Start-Sleep -Seconds 3 416 | } 417 | Catch { 418 | Debug "[ERROR] Archive Creation : $($Error[0])" 419 | Email "[ERROR] Archive Creation : Check Debug Log" 420 | Email "[ERROR] Archive Creation : $($Error[0])" 421 | EmailResults 422 | Exit 423 | } 424 | } 425 | 426 | 427 | <# Cycle Logs #> 428 | Function CycleLogs { 429 | Debug "----------------------------" 430 | Debug "Cycling Logs" 431 | $LogsToCycle | ForEach { 432 | $FullName = (Get-Item $_).FullName 433 | $BaseName = (Get-Item $_).BaseName 434 | $FileExt = (Get-Item $_).Extension 435 | If (Test-Path $FullName) { 436 | $NewLogName = $BaseName+"_"+$((Get-Date).ToString('yyyy-MM-dd'))+$FileExt 437 | Try { 438 | Rename-Item $FullName $NewLogName -ErrorAction Stop 439 | Debug "Cylcled $NewLogName" 440 | } 441 | Catch { 442 | Debug "[ERROR] $FullName log cycling ERROR : $($Error[0])" 443 | } 444 | } Else { 445 | Debug "[ERROR] $FullName not found" 446 | } 447 | } 448 | } 449 | 450 | <# Prune hMailServer logs #> 451 | Function PruneLogs { 452 | $FilesToDel = Get-ChildItem -Path "$hMSDir\Logs" | Where-Object {$_.LastWriteTime -lt ((Get-Date).AddDays(-$DaysToKeepLogs))} 453 | $CountDelLogs = $FilesToDel.Count 454 | If ($CountDelLogs -gt 0){ 455 | Debug "----------------------------" 456 | Debug "Begin pruning hMailServer logs older than $DaysToKeepLogs days" 457 | $EnumCountDelLogs = 0 458 | Try { 459 | $FilesToDel | ForEach { 460 | $FullName = $_.FullName 461 | $Name = $_.Name 462 | If (Test-Path $_.FullName -PathType Leaf) { 463 | Remove-Item -Force -Path $FullName 464 | Debug "Deleted file : $Name" 465 | $EnumCountDelLogs++ 466 | } 467 | } 468 | If ($CountDelLogs -eq $EnumCountDelLogs) { 469 | Debug "Successfully pruned $CountDelLogs hMailServer log$(Plural $CountDelLogs)" 470 | Email "[OK] Pruned hMailServer logs older than $DaysToKeepLogs days" 471 | } Else { 472 | Debug "[ERROR] Prune hMailServer logs : Filecount does not match delete count" 473 | Email "[ERROR] Prune hMailServer logs : Check Debug Log" 474 | } 475 | } 476 | Catch { 477 | Debug "[ERROR] Prune hMailServer logs : $($Error[0])" 478 | Email "[ERROR] Prune hMailServer logs : Check Debug Log" 479 | } 480 | } 481 | } 482 | 483 | <# Prune Backups Function #> 484 | Function PruneBackups { 485 | $FilesToDel = Get-ChildItem -Path $BackupLocation | Where-Object {$_.LastWriteTime -lt ((Get-Date).AddDays(-$DaysToKeepBackups))} 486 | $CountDel = $FilesToDel.Count 487 | If ($CountDel -gt 0) { 488 | Debug "----------------------------" 489 | Debug "Begin pruning local backups older than $DaysToKeepBackups days" 490 | $EnumCountDel = 0 491 | Try { 492 | $FilesToDel | ForEach { 493 | $FullName = $_.FullName 494 | $Name = $_.Name 495 | If (Test-Path $_.FullName -PathType Container) { 496 | Remove-Item -Force -Recurse -Path $FullName 497 | Debug "Deleted folder: $Name" 498 | $EnumCountDel++ 499 | } 500 | If (Test-Path $_.FullName -PathType Leaf) { 501 | Remove-Item -Force -Path $FullName 502 | Debug "Deleted file : $Name" 503 | $EnumCountDel++ 504 | } 505 | } 506 | If ($CountDel -eq $EnumCountDel) { 507 | Debug "Successfully pruned $CountDel item$(Plural $CountDel)" 508 | Email "[OK] Pruned backups older than $DaysToKeepBackups days" 509 | } Else { 510 | Debug "[ERROR] Prune backups : Filecount does not match delete count" 511 | Email "[ERROR] Prune backups : Check Debug Log" 512 | } 513 | } 514 | Catch { 515 | Debug "[ERROR] Prune backups : $($Error[0])" 516 | Email "[ERROR] Prune backups : Check Debug Log" 517 | } 518 | } 519 | } 520 | 521 | <# Prune Messages Functions #> 522 | Function GetSubFolders ($Folder) { 523 | $IterateFolder = 0 524 | $ArrayDeletedFolders = @() 525 | If ($Folder.SubFolders.Count -gt 0) { 526 | Do { 527 | $SubFolder = $Folder.SubFolders.Item($IterateFolder) 528 | $SubFolderName = $SubFolder.Name 529 | $SubFolderID = $SubFolder.ID 530 | If ($SubFolder.Subfolders.Count -gt 0) {GetSubFolders $SubFolder} 531 | If ($SubFolder.Messages.Count -gt 0) { 532 | If ($PruneSubFolders) {GetMessages $SubFolder} 533 | } Else { 534 | If ($PruneEmptySubFolders) {$ArrayDeletedFolders += $SubFolderID} 535 | } 536 | $IterateFolder++ 537 | } Until ($IterateFolder -eq $Folder.SubFolders.Count) 538 | } 539 | If ($PruneEmptySubFolders) { 540 | $ArrayDeletedFolders | ForEach { 541 | $CheckFolder = $Folder.SubFolders.ItemByDBID($_) 542 | If (SubFoldersEmpty $CheckFolder) { 543 | Try { 544 | If ($DoDelete) {$Folder.SubFolders.DeleteByDBID($_)} 545 | $TotalDeletedFolders++ 546 | Debug "Deleted empty subfolder $($CheckFolder.Name) in $($hMSAccount.Address)" 547 | } 548 | Catch { 549 | $DeleteFolderErrors++ 550 | Debug "[ERROR] Deleting empty subfolder $($CheckFolder.Name) in $($hMSAccount.Address)" 551 | Debug "[ERROR] : $($Error[0])" 552 | } 553 | $Error.Clear() 554 | } 555 | } 556 | } 557 | $ArrayDeletedFolders.Clear() 558 | } 559 | 560 | Function SubFoldersEmpty ($Folder) { 561 | $IterateFolder = 0 562 | If ($Folder.SubFolders.Count -gt 0) { 563 | Do { 564 | $SubFolder = $Folder.SubFolders.Item($IterateFolder) 565 | If ($SubFolder.Messages.Count -gt 0) { 566 | Return $False 567 | Break 568 | } 569 | If ($SubFolder.SubFolders.Count -gt 0) { 570 | SubFoldersEmpty $SubFolder 571 | } 572 | $IterateFolder++ 573 | } Until ($IterateFolder -eq $Folder.SubFolders.Count) 574 | } Else { 575 | Return $True 576 | } 577 | } 578 | 579 | Function GetMatchFolders ($Folder) { 580 | $IterateFolder = 0 581 | If ($Folder.SubFolders.Count -gt 0) { 582 | Do { 583 | $SubFolder = $Folder.SubFolders.Item($IterateFolder) 584 | $SubFolderName = $SubFolder.Name 585 | If ($SubFolderName -match $PruneFolders) { 586 | GetSubFolders $SubFolder 587 | GetMessages $SubFolder 588 | } Else { 589 | GetMatchFolders $SubFolder 590 | } 591 | $IterateFolder++ 592 | } Until ($IterateFolder -eq $Folder.SubFolders.Count) 593 | } 594 | } 595 | 596 | Function GetMessages ($Folder) { 597 | $IterateMessage = 0 598 | $ArrayMessagesToDelete = @() 599 | $DeletedMessages = 0 600 | If ($Folder.Messages.Count -gt 0) { 601 | Do { 602 | $Message = $Folder.Messages.Item($IterateMessage) 603 | If ($Message.InternalDate -lt ((Get-Date).AddDays(-$DaysBeforeDelete))) { 604 | $ArrayMessagesToDelete += $Message.ID 605 | } 606 | $IterateMessage++ 607 | } Until ($IterateMessage -eq $Folder.Messages.Count) 608 | } 609 | $ArrayMessagesToDelete | ForEach { 610 | Try { 611 | If ($DoDelete) {$Folder.Messages.DeleteByDBID($_)} 612 | $DeletedMessages++ 613 | $TotalDeletedMessages++ 614 | } 615 | Catch { 616 | $DeleteMessageErrors++ 617 | Debug "[ERROR] Deleting messages from folder $($Folder.Name) in $($hMSAccount.Address)" 618 | Debug "[ERROR] $($Error[0])" 619 | } 620 | $Error.Clear() 621 | } 622 | If ($DeletedMessages -gt 0) { 623 | Debug "Deleted $DeletedMessages message$(Plural $DeletedMessages) from $($Folder.Name) in $($hMSAccount.Address)" 624 | } 625 | $ArrayMessagesToDelete.Clear() 626 | } 627 | 628 | Function PruneMessages { 629 | 630 | $Error.Clear() 631 | $BeginDeletingOldMessages = Get-Date 632 | Debug "----------------------------" 633 | Debug "Begin pruning messages older than $DaysBeforeDelete days" 634 | If (-not($DoDelete)) { 635 | Debug "Delete disabled - Test Run ONLY" 636 | } 637 | 638 | <# Authenticate hMailServer COM #> 639 | $hMS = New-Object -COMObject hMailServer.Application 640 | $AuthAdmin = $hMS.Authenticate("Administrator", $hMSAdminPass) 641 | 642 | If ($AuthAdmin) { 643 | 644 | <# Set required variables if blank #> 645 | If ([string]::IsNullOrEmpty($PruneFolders)) {$PruneFolders = "deleted|spam|junk"} 646 | If ([string]::IsNullOrEmpty($SkipAccountPruning)) {$SkipAccountPruning = $(-Join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | % {[Char]$_}))} 647 | If ([string]::IsNullOrEmpty($SkipDomainPruning)) {$SkipDomainPruning = $(-Join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | % {[Char]$_}))} 648 | 649 | <# Loop through IMAP folders #> 650 | If ($hMS.Domains.Count -gt 0) { 651 | For ($IterateDomains = 0; $IterateDomains -lt $hMS.Domains.Count; $IterateDomains++) { 652 | $hMSDomain = $hMS.Domains.Item($IterateDomains) 653 | If (($hMSDomain.Active) -and ($hMSDomain.Name -notmatch [regex]$SkipDomainPruning) -and ($hMSDomain.Accounts.Count -gt 0)) { 654 | For ($IterateAccounts = 0; $IterateAccounts -lt $hMSDomain.Accounts.Count; $IterateAccounts++) { 655 | $hMSAccount = $hMSDomain.Accounts.Item($IterateAccounts) 656 | If (($hMSAccount.Active) -and ($hMSAccount.Address -notmatch [regex]$SkipAccountPruning) -and ($hMSAccount.IMAPFolders.Count -gt 0)) { 657 | For ($IterateIMAPFolders = 0; $IterateIMAPFolders -lt $hMSAccount.IMAPFolders.Count; $IterateIMAPFolders++) { 658 | $hMSIMAPFolder = $hMSAccount.IMAPFolders.Item($IterateIMAPFolders) 659 | If ($hMSIMAPFolder.Name -match $PruneFolders) { 660 | If ($hMSIMAPFolder.SubFolders.Count -gt 0) { 661 | GetSubFolders $hMSIMAPFolder 662 | } 663 | GetMessages $hMSIMAPFolder 664 | } Else { 665 | GetMatchFolders $hMSIMAPFolder 666 | } 667 | } 668 | } 669 | } 670 | } 671 | } 672 | } 673 | 674 | <# Report message pruning #> 675 | If ($DeleteMessageErrors -gt 0) { 676 | Debug "Finished Message Pruning : $DeleteMessageErrors Errors present" 677 | Email "[ERROR] Message Pruning : $DeleteMessageErrors Errors present : Check debug log" 678 | } 679 | If ($TotalDeletedMessages -gt 0) { 680 | Debug "Finished pruning $TotalDeletedMessages messages in $(ElapsedTime $BeginDeletingOldMessages)" 681 | Email "[OK] Pruned $TotalDeletedMessages messages older than $DaysBeforeDelete days" 682 | } Else { 683 | Debug "No messages older than $DaysBeforeDelete days to prune" 684 | Email "[OK] No messages older than $DaysBeforeDelete days to prune" 685 | } 686 | 687 | <# Report folder pruning #> 688 | If ($DeleteFolderErrors -gt 0) { 689 | Debug "Deleting Empty Folders : $DeleteFolderErrors Errors present" 690 | Email "[ERROR] Deleting Empty Folders : $DeleteFolderErrors Errors present : Check debug log" 691 | } 692 | If ($TotalDeletedFolders -gt 0) { 693 | Debug "Deleted $TotalDeletedFolders empty subfolders" 694 | Email "[OK] Deleted $TotalDeletedFolders empty subfolders" 695 | } Else { 696 | Debug "No empty subfolders deleted" 697 | # Email "[OK] No empty subfolders deleted" 698 | } 699 | 700 | } Else { 701 | Debug "[ERROR] hMailServer COM authentication failed. Check password and/or Windows Event Log." 702 | Email "[ERROR] hMailServer COM authentication failed. Check password and/or Windows Event Log." 703 | } 704 | } 705 | 706 | <# Feed Bayes #> 707 | Function GetBayesSubFolders ($Folder) { 708 | $IterateFolder = 0 709 | $ArrayBayesMessages = @() 710 | If ($Folder.SubFolders.Count -gt 0) { 711 | Do { 712 | $SubFolder = $Folder.SubFolders.Item($IterateFolder) 713 | $SubFolderName = $SubFolder.Name 714 | $SubFolderID = $SubFolder.ID 715 | If ($SubFolder.Subfolders.Count -gt 0) {GetBayesSubFolders $SubFolder} 716 | If ($SubFolder.Messages.Count -gt 0) { 717 | If ($BayesSubFolders) {GetBayesMessages $SubFolder} 718 | } 719 | $IterateFolder++ 720 | } Until ($IterateFolder -eq $Folder.SubFolders.Count) 721 | } 722 | $ArrayBayesMessages.Clear() 723 | } 724 | 725 | Function GetBayesMatchFolders ($Folder) { 726 | $IterateFolder = 0 727 | If ($Folder.SubFolders.Count -gt 0) { 728 | Do { 729 | $SubFolder = $Folder.SubFolders.Item($IterateFolder) 730 | $SubFolderName = $SubFolder.Name 731 | If (($SubFolderName -match $HamFolders) -or ($SubFolderName -match $SpamFolders)) { 732 | GetBayesSubFolders $SubFolder 733 | GetBayesMessages $SubFolder 734 | } Else { 735 | GetBayesMatchFolders $SubFolder 736 | } 737 | $IterateFolder++ 738 | } Until ($IterateFolder -eq $Folder.SubFolders.Count) 739 | } 740 | } 741 | 742 | Function GetBayesMessages ($Folder) { 743 | $IterateMessage = 0 744 | $ArrayHamToFeed = @() 745 | $ArraySpamToFeed = @() 746 | $HamFedMessages = 0 747 | $SpamFedMessages = 0 748 | $LearnedHamMessagesFolder = 0 749 | $LearnedSpamMessagesFolder = 0 750 | If ($Folder.Messages.Count -gt 0) { 751 | If ($Folder.Name -match $HamFolders) { 752 | Do { 753 | $Message = $Folder.Messages.Item($IterateMessage) 754 | If ($Message.InternalDate -gt ((Get-Date).AddDays(-$BayesDays))) { 755 | $ArrayHamToFeed += $Message.FileName 756 | } 757 | $IterateMessage++ 758 | } Until ($IterateMessage -eq $Folder.Messages.Count) 759 | } 760 | If ($Folder.Name -match $SpamFolders) { 761 | Do { 762 | $Message = $Folder.Messages.Item($IterateMessage) 763 | If ($Message.InternalDate -gt ((Get-Date).AddDays(-$BayesDays))) { 764 | $ArraySpamToFeed += $Message.FileName 765 | } 766 | $IterateMessage++ 767 | } Until ($IterateMessage -eq $Folder.Messages.Count) 768 | } 769 | } 770 | $ArrayHamToFeed | ForEach { 771 | $FileName = $_ 772 | Try { 773 | If ((Get-Item $FileName).Length -lt 512000) { 774 | If ($DoSpamC) { 775 | $SpamC = & cmd /c "`"$SADir\spamc.exe`" -d `"$SAHost`" -p `"$SAPort`" -L ham < `"$FileName`"" 776 | $SpamCResult = Out-String -InputObject $SpamC 777 | If ($SpamCResult -match "Message successfully un/learned") { 778 | $LearnedHamMessages++ 779 | $LearnedHamMessagesFolder++ 780 | } 781 | If (($SpamCResult -notmatch "Message successfully un/learned") -and ($SpamCResult -notmatch "Message was already un/learned")) { 782 | Throw $SpamCResult 783 | } 784 | } 785 | $HamFedMessages++ 786 | $TotalHamFedMessages++ 787 | } 788 | } 789 | Catch { 790 | $HamFedMessageErrors++ 791 | Debug "[ERROR] Feeding HAM message $FileName in $($hMSAccount.Address)" 792 | Debug "[ERROR] $($Error[0])" 793 | } 794 | } 795 | $ArraySpamToFeed | ForEach { 796 | $FileName = $_ 797 | Try { 798 | If ((Get-Item $FileName).Length -lt 512000) { 799 | If ($DoSpamC) { 800 | $SpamC = & cmd /c "`"$SADir\spamc.exe`" -d `"$SAHost`" -p `"$SAPort`" -L spam < `"$FileName`"" 801 | $SpamCResult = Out-String -InputObject $SpamC 802 | If ($SpamCResult -match "Message successfully un/learned") { 803 | $LearnedSpamMessages++ 804 | $LearnedSpamMessagesFolder++ 805 | } 806 | If (($SpamCResult -notmatch "Message successfully un/learned") -and ($SpamCResult -notmatch "Message was already un/learned")) { 807 | Throw $SpamCResult 808 | } 809 | } 810 | $SpamFedMessages++ 811 | $TotalSpamFedMessages++ 812 | } 813 | } 814 | Catch { 815 | $SpamFedMessageErrors++ 816 | Debug "[ERROR] Feeding SPAM message $FileName in $($hMSAccount.Address)" 817 | Debug "[ERROR] $($Error[0])" 818 | } 819 | } 820 | If ($HamFedMessages -gt 0) { 821 | Debug "Learned tokens from $LearnedHamMessagesFolder of $HamFedMessages HAM message$(Plural $HamFedMessages) fed from $($Folder.Name) in $($hMSAccount.Address)" 822 | } 823 | If ($SpamFedMessages -gt 0) { 824 | Debug "Learned tokens from $LearnedSpamMessagesFolder of $SpamFedMessages SPAM message$(Plural $SpamFedMessages) fed from $($Folder.Name) in $($hMSAccount.Address)" 825 | } 826 | $ArraySpamToFeed.Clear() 827 | } 828 | 829 | Function FeedBayes { 830 | 831 | $Error.Clear() 832 | 833 | $BeginFeedingBayes = Get-Date 834 | Debug "----------------------------" 835 | Debug "Begin learning Bayes tokens from messages newer than $BayesDays days" 836 | If (-not($DoSpamC)) { 837 | Debug "SpamC disabled - Test Run ONLY" 838 | } 839 | 840 | <# Authenticate hMailServer COM #> 841 | $hMS = New-Object -COMObject hMailServer.Application 842 | $AuthAdmin = $hMS.Authenticate("Administrator", $hMSAdminPass) 843 | 844 | If ($AuthAdmin) { 845 | 846 | <# Set SA host and port variables #> 847 | $SAHost = $hMS.Settings.AntiSpam.SpamAssassinHost 848 | $SAPort = $hMS.Settings.AntiSpam.SpamAssassinPort 849 | 850 | <# Set required variables if blank #> 851 | If ([string]::IsNullOrEmpty($HamFolders)) {$HamFolders = "inbox"} 852 | If ([string]::IsNullOrEmpty($SpamFolders)) {$SpamFolders = "spam|junk"} 853 | If ([string]::IsNullOrEmpty($SkipAccountBayes)) {$SkipAccountBayes = $(-Join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | ForEach {[Char]$_}))} 854 | If ([string]::IsNullOrEmpty($SkipDomainBayes)) {$SkipDomainBayes = $(-Join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | ForEach {[Char]$_}))} 855 | 856 | <# Loop through IMAP folders #> 857 | If ($hMS.Domains.Count -gt 0) { 858 | For ($IterateDomains = 0; $IterateDomains -lt $hMS.Domains.Count; $IterateDomains++) { 859 | $hMSDomain = $hMS.Domains.Item($IterateDomains) 860 | If (($hMSDomain.Active) -and ($hMSDomain.Name -notmatch [regex]$SkipDomainBayes) -and ($hMSDomain.Accounts.Count -gt 0)) { 861 | For ($IterateAccounts = 0; $IterateAccounts -lt $hMSDomain.Accounts.Count; $IterateAccounts++) { 862 | $hMSAccount = $hMSDomain.Accounts.Item($IterateAccounts) 863 | If (($hMSAccount.Active) -and ($hMSAccount.Address -notmatch [regex]$SkipAccountBayes) -and ($hMSAccount.IMAPFolders.Count -gt 0)) { 864 | For ($IterateIMAPFolders = 0; $IterateIMAPFolders -lt $hMSAccount.IMAPFolders.Count; $IterateIMAPFolders++) { 865 | $hMSIMAPFolder = $hMSAccount.IMAPFolders.Item($IterateIMAPFolders) 866 | If (($hMSIMAPFolder.Name -match $HamFolders) -or ($hMSIMAPFolder.Name -match $SpamFolders)) { 867 | If ($hMSIMAPFolder.SubFolders.Count -gt 0) { 868 | GetBayesSubFolders $hMSIMAPFolder 869 | } 870 | GetBayesMessages $hMSIMAPFolder 871 | } Else { 872 | GetBayesMatchFolders $hMSIMAPFolder 873 | } 874 | } 875 | } 876 | } 877 | } 878 | } 879 | } 880 | 881 | Debug "----------------------------" 882 | Debug "Finished feeding $($TotalHamFedMessages + $TotalSpamFedMessages) messages to Bayes in $(ElapsedTime $BeginFeedingBayes)" 883 | Debug "----------------------------" 884 | 885 | If ($HamFedMessageErrors -gt 0) { 886 | Debug "Errors feeding HAM to SpamC : $HamFedMessageErrors Error$(Plural $HamFedMessageErrors) present" 887 | Email "[ERROR] HAM SpamC : $HamFedMessageErrors Errors present : Check debug log" 888 | } 889 | If ($TotalHamFedMessages -gt 0) { 890 | Debug "Bayes learned from $LearnedHamMessages of $TotalHamFedMessages HAM message$(Plural $TotalHamFedMessages) found" 891 | Email "[OK] Bayes HAM from $LearnedHamMessages of $TotalHamFedMessages message$(Plural $TotalHamFedMessages)" 892 | } Else { 893 | Debug "No HAM messages newer than $BayesDays days to feed to Bayes" 894 | Email "[OK] No HAM messages to feed Bayes" 895 | } 896 | 897 | If ($SpamFedMessageErrors -gt 0) { 898 | Debug "Errors feeding SPAM to SpamC : $SpamFedMessageErrors Error$(Plural $SpamFedMessageErrors) present" 899 | Email "[ERROR] SPAM SpamC : $SpamFedMessageErrors Errors present : Check debug log" 900 | } 901 | If ($TotalSpamFedMessages -gt 0) { 902 | Debug "Bayes learned from $LearnedSpamMessages of $TotalSpamFedMessages SPAM message$(Plural $TotalSpamFedMessages) found" 903 | Email "[OK] Bayes SPAM from $LearnedSpamMessages of $TotalSpamFedMessages message$(Plural $TotalSpamFedMessages)" 904 | } Else { 905 | Debug "No SPAM messages newer than $BayesDays days to feed to Bayes" 906 | Email "[OK] No SPAM messages to feed Bayes" 907 | } 908 | 909 | If ($SyncBayesJournal) { 910 | Debug "----------------------------" 911 | Try { 912 | $BayesSync = & cmd /c "`"$SADir\sa-learn.exe`" --sync" 913 | $BayesSyncResult = Out-String -InputObject $BayesSync 914 | If ([string]::IsNullOrEmpty($BayesSyncResult)) { 915 | Throw "Nothing to sync" 916 | } 917 | Debug $BayesSyncResult 918 | } 919 | Catch { 920 | Debug "[INFO] Bayes Journal Sync: $($Error[0])" 921 | } 922 | } 923 | 924 | If ($BackupBayesDatabase) { 925 | Debug "----------------------------" 926 | Try { 927 | If (-not(Test-Path $BayesBackupLocation)) { 928 | Throw "Bayes backup file does not exist - Check Path" 929 | } Else { 930 | & cmd /c "`"$SADir\sa-learn.exe`" --backup > `"$BayesBackupLocation`"" 931 | If ((Get-Item -Path $BayesBackupLocation).LastWriteTime -lt ((Get-Date).AddSeconds(-30))) { 932 | Throw "Unknown Error backing up Bayes database" 933 | } 934 | Debug "Successfully backed up Bayes database" 935 | } 936 | } 937 | Catch { 938 | Debug "[ERROR] Backing up Bayes : $($Error[0])" 939 | Email "[ERROR] Backing up Bayes db" 940 | } 941 | } 942 | 943 | 944 | } Else { 945 | Debug "[ERROR] hMailServer COM authentication failed. Check password and/or Windows Event Log." 946 | Email "[ERROR] hMailServer COM authentication failed. Check password and/or Windows Event Log." 947 | } 948 | 949 | } 950 | 951 | <# Check for updates #> 952 | Function CheckForUpdates { 953 | Debug "----------------------------" 954 | Debug "Checking for script update at GitHub" 955 | $GitHubVersion = $LocalVersion = $NULL 956 | $GetGitHubVersion = $GetLocalVersion = $False 957 | $GitHubVersionTries = 1 958 | Do { 959 | Try { 960 | $GitHubVersion = [decimal](Invoke-WebRequest -UseBasicParsing -Method GET -URI https://raw.githubusercontent.com/palinkas-jo-reggelt/hMailServer_Offsite_Backup/main/version.txt).Content 961 | $GetGitHubVersion = $True 962 | } 963 | Catch { 964 | Debug "[ERROR] Obtaining GitHub version : Try $GitHubVersionTries : Obtaining version number: $($Error[0])" 965 | } 966 | $GitHubVersionTries++ 967 | } Until (($GitHubVersion -gt 0) -or ($GitHubVersionTries -eq 6)) 968 | If (Test-Path "$PSScriptRoot\version.txt") { 969 | $LocalVersion = [decimal](Get-Content "$PSScriptRoot\version.txt") 970 | $GetLocalVersion = $True 971 | } 972 | If (($GetGitHubVersion) -and ($GetLocalVersion)) { 973 | If ($LocalVersion -lt $GitHubVersion) { 974 | Debug "[INFO] Upgrade to version $GitHubVersion available at https://github.com/palinkas-jo-reggelt/hMailServer_Offsite_Backup" 975 | If ($UseHTML) { 976 | Email "[INFO] Upgrade to version $GitHubVersion available at GitHub" 977 | } Else { 978 | Email "[INFO] Upgrade to version $GitHubVersion available at https://github.com/palinkas-jo-reggelt/hMailServer_Offsite_Backup" 979 | } 980 | } Else { 981 | Debug "Backup & Upload script is latest version: $GitHubVersion" 982 | } 983 | } Else { 984 | If ((-not($GetGitHubVersion)) -and (-not($GetLocalVersion))) { 985 | Debug "[ERROR] Version test failed : Could not obtain either GitHub nor local version information" 986 | Email "[ERROR] Version check failed" 987 | } ElseIf (-not($GetGitHubVersion)) { 988 | Debug "[ERROR] Version test failed : Could not obtain version information from GitHub" 989 | Email "[ERROR] Version check failed" 990 | } ElseIf (-not($GetLocalVersion)) { 991 | Debug "[ERROR] Version test failed : Could not obtain local install version information" 992 | Email "[ERROR] Version check failed" 993 | } Else { 994 | Debug "[ERROR] Version test failed : Unknown reason - file issue at GitHub" 995 | Email "[ERROR] Version check failed" 996 | } 997 | } 998 | } --------------------------------------------------------------------------------