aboutsummaryrefslogtreecommitdiff
path: root/writeups/powershell-email/sendmail.ps1
blob: e5500ecc54469ac71af646bd99ccf9373c385e6b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env pwsh

# Send an email using Powershell.
# Usage: sendmail.ps1 [attachment 1 [attachment 2 [... attachment N]]]
#
# This is a POC on how to send an email using Powershell. The script should work
# on all platforms. The information required for the script to work is all
# supplied via environment variables.
#
# Env Vars
#   smtp_host
#   smtp_port (best if you let the implementation decide)
#   smtp_tls:
#     'F' to insist on secure connection (default)
#     'O' for opportunistic
#     'N' to disable (default if $smtp_host is "localhost")
#   smtp_tls_cert
#   smtp_username
#   smtp_password
#   mail_from (required)
#   mail_to (required)
#   mail_subject (required)
#
# Example
#```pwsh
#   echo 'Hi! it going? Testing my Powershell script.' | \
#     smtp_host=smtp.gmail.com \
#     smtp_username=example@gmail.com \
#     smtp_password='0123456789' \
#     mail_from=alice@gmail.com \
#     mail_to=bob@example.com \
#     mail_subject='Sent using Powershell' \
#      sendmail.ps1 \
#        doc.pdf
#```
using namespace System
using namespace System.Net
using namespace System.Security.Cryptography

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$PSDefaultParameterValues['*:ErrorAction'] = 'Stop'

<#
.SYNOPSIS
Get an environment variable and unset it

.PARAMETER Name
The name of the environment variable to read and unset

.PARAMETER Required
If set, throw FileNotFoundException if the environment variable requested is not
set

.NOTES
The purpose of the function is to get and scrub off the password passed as an
env var in one go. To preserve the env var, use `GetEnvironmentVariable()`
directly.
#>
function FetchEnv ([string]$Name, [bool]$Required = $false) {
	$RetVal = [Environment]::GetEnvironmentVariable($Name)
	[Environment]::SetEnvironmentVariable($Name, '')
	if ( $RetVal ) {
		return $RetVal
	}
	else {
		if ($Required) {
			throw New-Object IO.FileNotFoundException ("${Name}: unset env var")
		}
		else {
			return $null
		}
	}
}

<#
.SYNOPSIS
Read data from STDIN until EOF and return data as decoded string
#>
function ExhaustStdin () {
	$stream = New-Object IO.StreamReader ( [Console]::OpenStandardInput() )
	return $stream.ReadToEnd()
}


######################################################################
# Execution starts here
######################################################################

# Compose a message
$mail = New-Object Mail.MailMessage (
	(FetchEnv "mail_from" $true),
	(FetchEnv "mail_to" $true),
	(FetchEnv "mail_subject" $true),
	(ExhaustStdin)) # This is the part where the mail body is read from STDIN

# Add attachments
foreach ($file in $args) {
	[string]$file = $file

	$a = New-Object Mail.Attachment (
		$file,
		# Treat all attachments as binary
		[System.Net.Mime.MediaTypeNames+Application]::Octet)
	# Timestamp support
	$a.ContentDisposition.CreationDate = [IO.File]::GetCreationTime($file)
	$a.ContentDisposition.ModificationDate = [IO.File]::GetLastWriteTime($file)
	$a.ContentDisposition.ReadDate = [IO.File]::GetLastAccessTime($file)

	$mail.Attachments.Add($a)
}

# Set up credentials
$client_cred = New-Object NetworkCredential (
	(FetchEnv "smtp_username"),
	(FetchEnv "smtp_password"))

# Set up client TLS cert
$tls_cert = FetchEnv("smtp_tls_cert")
if ($null -ne $tls_cert) {
	$cert_chain = New-Object X509Certificates.X509Certificate ( $tls_cert )
}
else {
	$cert_chain = $null
}

# Read target SMTP host
$smtp_host = FetchEnv "smtp_host"
if (!$smtp_host) {
	$smtp_host = "localhost"
}

# Set up SMTP client object
$smtp = New-Object Mail.SmtpClient ($smtp_host)
if ($cert_chain) {
	$smtp.ClientCertificates.Add($cert_chain)
}
$smtp.Credentials = $client_cred
$smtp_port = FetchEnv "smtp_port"
if ($smtp_port) {
	$smtp.Port = [int]$smtp_port
}

$tlsmode = FetchEnv "smtp_tls"
if (!$tlsmode) {
	# Determine the "tlsmode" to default to
	if ($smtp_host -eq "localhost") {
		# No need to waste computing power on TLS.
		# Unless you're paranoid and don't trust the hosts file.
		$tlsmode = "N"
	}
	else {
		# Transmitting plain text data on the internet nowadays is no-brainer.
		# Most email services will refuse anyway.
		$tlsmode = "F"
	}
}

# Set `$smtp.EnableSsl` based on `$tlsmode`
if ($tlsmode -eq "F" -or $tlsmode -eq "O") {
	$smtp.EnableSsl = $true
}
elseif ($tlsmode -eq "N") {
	$smtp.EnableSsl = $false
}

try {
	$smtp.Send($mail)
}
catch {
	if ($tlsmode -eq "O") {
		# Opportunistic tlsmode. Try again with TLS disabled.
		# Please think twice and fix the problem before resorting to this bit.
		$smtp.EnableSsl = $false
		$smtp.Send($mail)
	}
	else {
		# Let the script die
		throw $_
	}
}