Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

173 lines
8.7KB

  1. $ErrorActionPreference = 'Stop'
  2. . (Join-Path $PSScriptRoot 'mail-common.ps1')
  3. function Get-TemplateLimits {
  4. param($Context)
  5. $settings = $Context.Settings
  6. if ($settings['TEMPLATE_IMAGE_MODE'] -ne 'inline_cid') { throw 'TEMPLATE_IMAGE_MODE must be inline_cid' }
  7. $types = @($settings['TEMPLATE_IMAGE_TYPES'].Split(',') | ForEach-Object { $_.Trim().ToLowerInvariant() })
  8. if (-not $types.Count -or @($types | Where-Object { $_ -notin @('png', 'jpeg', 'gif') }).Count) {
  9. throw 'TEMPLATE_IMAGE_TYPES must contain only png, jpeg, and gif'
  10. }
  11. $imageLimit = 0
  12. $messageLimit = 0
  13. if (-not [int]::TryParse($settings['TEMPLATE_IMAGE_MAX_BYTES'], [ref]$imageLimit) -or $imageLimit -lt 1 -or $imageLimit -gt 10485760) {
  14. throw 'Invalid TEMPLATE_IMAGE_MAX_BYTES'
  15. }
  16. if (-not [int]::TryParse($settings['TEMPLATE_MESSAGE_MAX_BYTES'], [ref]$messageLimit) -or $messageLimit -lt $imageLimit -or $messageLimit -gt 52428800) {
  17. throw 'Invalid TEMPLATE_MESSAGE_MAX_BYTES'
  18. }
  19. $root = $settings['TEMPLATE_ROOT']
  20. if (-not [System.IO.Path]::IsPathRooted($root)) {
  21. $root = Join-Path (Split-Path -Parent $PSScriptRoot) $root
  22. }
  23. return @{ Root = [System.IO.Path]::GetFullPath($root); Types = $types;
  24. ImageLimit = $imageLimit; MessageLimit = $messageLimit }
  25. }
  26. function Get-HashHex {
  27. param([byte[]]$Bytes)
  28. $hash = [System.Security.Cryptography.SHA256]::Create()
  29. try { return ([BitConverter]::ToString($hash.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant() }
  30. finally { $hash.Dispose() }
  31. }
  32. function Assert-SafeStyle {
  33. param([string]$Style)
  34. $allowed = @('color', 'background-color', 'font-family', 'font-size', 'font-weight',
  35. 'text-align', 'text-decoration', 'line-height', 'margin', 'margin-top', 'margin-bottom',
  36. 'padding', 'padding-top', 'padding-bottom', 'border', 'border-color', 'border-width',
  37. 'border-style', 'width', 'height', 'display')
  38. foreach ($declaration in $Style.Split(';')) {
  39. $part = $declaration.Trim()
  40. if (-not $part) { continue }
  41. $match = [regex]::Match($part, '^([a-z-]+)\s*:\s*(.+)$', 'IgnoreCase')
  42. if (-not $match.Success -or $match.Groups[1].Value.ToLowerInvariant() -notin $allowed) {
  43. throw "Unsupported CSS declaration: $part"
  44. }
  45. $value = $match.Groups[2].Value
  46. if ($value -notmatch "^[a-zA-Z0-9# ,.%()'-]+$" -or
  47. $value -match '(?i)(expression|url|behavior|import|javascript|vbscript|position|fixed)') {
  48. throw "Unsafe CSS value: $part"
  49. }
  50. }
  51. }
  52. function Assert-TemplateNode {
  53. param([System.Xml.XmlNode]$Node)
  54. if ($Node.NodeType -eq [System.Xml.XmlNodeType]::Comment) { throw 'HTML comments are not supported' }
  55. if ($Node.NodeType -eq [System.Xml.XmlNodeType]::Text -or
  56. $Node.NodeType -eq [System.Xml.XmlNodeType]::Whitespace -or
  57. $Node.NodeType -eq [System.Xml.XmlNodeType]::SignificantWhitespace) { return }
  58. if ($Node.NodeType -ne [System.Xml.XmlNodeType]::Element) { throw 'Unsupported HTML node' }
  59. $tags = @('html', 'head', 'title', 'body', 'div', 'span', 'p', 'br', 'hr', 'h1', 'h2',
  60. 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'b', 'i', 'u', 'ul', 'ol', 'li', 'table',
  61. 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th', 'a', 'img')
  62. $tag = $Node.LocalName.ToLowerInvariant()
  63. if ($Node.NamespaceURI -and $Node.NamespaceURI -ne 'http://www.w3.org/1999/xhtml') {
  64. throw "Unsupported namespace on <$tag>"
  65. }
  66. if ($tag -notin $tags) { throw "Unsupported HTML element: <$tag>" }
  67. foreach ($attribute in $Node.Attributes) {
  68. $name = $attribute.Name.ToLowerInvariant()
  69. $value = $attribute.Value
  70. if ($name -eq 'xmlns' -and $tag -eq 'html' -and
  71. $value -eq 'http://www.w3.org/1999/xhtml') { continue }
  72. if ($name -eq 'style') { Assert-SafeStyle $value; continue }
  73. if ($name -eq 'class' -and $value -match '^[a-zA-Z0-9_ -]{1,100}$') { continue }
  74. if ($name -eq 'alt' -and $tag -eq 'img') { continue }
  75. if ($name -eq 'src' -and $tag -eq 'img') { continue }
  76. if ($name -eq 'href' -and $tag -eq 'a' -and
  77. $value -match '^(https?://[^\s<>"'']+|mailto:[^\s<>"'']+)$') { continue }
  78. if ($name -in @('width', 'height', 'border', 'cellpadding', 'cellspacing') -and
  79. $value -match '^[0-9]{1,4}(%|px)?$') { continue }
  80. if ($name -eq 'align' -and $value -in @('left', 'center', 'right')) { continue }
  81. if ($name -eq 'valign' -and $value -in @('top', 'middle', 'bottom')) { continue }
  82. if ($name -eq 'bgcolor' -and $value -match '^#[0-9a-fA-F]{6}$') { continue }
  83. throw "Unsupported or unsafe attribute $name on <$tag>"
  84. }
  85. foreach ($child in $Node.ChildNodes) { Assert-TemplateNode $child }
  86. }
  87. function Read-SafeTemplate {
  88. param([string]$Path)
  89. if ([System.IO.Path]::GetExtension($Path).ToLowerInvariant() -notin @('.html', '.htm')) {
  90. throw 'Select a .html or .htm template'
  91. }
  92. return Read-SafeTemplateXml $Path $false
  93. }
  94. function Read-SafeTemplateXml {
  95. param([string]$Source, [bool]$IsText)
  96. $readerSettings = New-Object System.Xml.XmlReaderSettings
  97. $readerSettings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit
  98. $readerSettings.XmlResolver = $null
  99. $readerSettings.MaxCharactersInDocument = 2097152
  100. if ($IsText) {
  101. $sourceReader = New-Object System.IO.StringReader($Source)
  102. $reader = [System.Xml.XmlReader]::Create($sourceReader, $readerSettings)
  103. }
  104. else { $reader = [System.Xml.XmlReader]::Create($Source, $readerSettings) }
  105. try {
  106. $document = New-Object System.Xml.XmlDocument
  107. $document.XmlResolver = $null
  108. $document.Load($reader)
  109. }
  110. finally {
  111. $reader.Dispose()
  112. if ($IsText) { $sourceReader.Dispose() }
  113. }
  114. if ($document.DocumentElement.LocalName.ToLowerInvariant() -ne 'html') {
  115. throw 'Template must have an <html> root'
  116. }
  117. Assert-TemplateNode $document.DocumentElement
  118. if (-not @($document.DocumentElement.ChildNodes | Where-Object { $_.LocalName -eq 'body' }).Count) {
  119. throw 'Template must have a <body> element'
  120. }
  121. return $document
  122. }
  123. function Get-TemplateImages {
  124. param($Document, [string]$TemplatePath, $Limits)
  125. $base = [System.IO.Path]::GetDirectoryName($TemplatePath)
  126. $images = New-Object System.Collections.ArrayList
  127. $seen = @{}
  128. foreach ($node in $Document.GetElementsByTagName('img')) {
  129. $relative = $node.GetAttribute('src')
  130. if (-not $relative -or $relative -notmatch '^[a-zA-Z0-9_][a-zA-Z0-9_./ -]*$' -or
  131. $relative -match '(^|/)\.\.?(/|$)' -or $relative.Contains('//')) {
  132. throw "Image src must be a simple relative path: $relative"
  133. }
  134. $imagePath = [System.IO.Path]::GetFullPath((Join-Path $base $relative))
  135. if (-not $imagePath.StartsWith($base + [System.IO.Path]::DirectorySeparatorChar,
  136. [StringComparison]::OrdinalIgnoreCase)) { throw "Image is outside template folder: $relative" }
  137. if (-not (Test-Path -LiteralPath $imagePath -PathType Leaf)) { throw "Missing image: $relative" }
  138. $ext = [System.IO.Path]::GetExtension($imagePath).ToLowerInvariant()
  139. $type = switch ($ext) { '.png' { 'png' } '.jpg' { 'jpeg' } '.jpeg' { 'jpeg' } '.gif' { 'gif' } default { '' } }
  140. if ($type -notin $Limits.Types) { throw "Image type is not enabled: $relative" }
  141. $length = (Get-Item -LiteralPath $imagePath).Length
  142. if ($length -lt 8 -or $length -gt $Limits.ImageLimit) { throw "Image size is outside the configured limit: $relative" }
  143. if (-not $seen.ContainsKey($relative)) {
  144. $bytes = [System.IO.File]::ReadAllBytes($imagePath)
  145. $valid = switch ($type) {
  146. 'png' { $bytes.Length -ge 8 -and ([BitConverter]::ToString($bytes,0,8) -eq '89-50-4E-47-0D-0A-1A-0A') }
  147. 'jpeg' { $bytes.Length -ge 4 -and $bytes[0] -eq 255 -and $bytes[1] -eq 216 -and $bytes[-2] -eq 255 -and $bytes[-1] -eq 217 }
  148. 'gif' { $bytes.Length -ge 6 -and [System.Text.Encoding]::ASCII.GetString($bytes,0,6) -in @('GIF87a','GIF89a') }
  149. }
  150. if (-not $valid) { throw "Image signature does not match its type: $relative" }
  151. $cid = 'mm-' + [guid]::NewGuid().ToString('N') + '@local'
  152. $seen[$relative] = $cid
  153. $null = $images.Add(@{ Path = $relative; Bytes = $bytes; Type = 'image/' + $type;
  154. Hash = Get-HashHex $bytes; Cid = $cid })
  155. }
  156. $node.SetAttribute('src', 'cid:' + $seen[$relative])
  157. }
  158. return ,$images
  159. }
  160. function Add-TextParameter {
  161. param($Command, [string]$Name, [int]$Size, [string]$Value)
  162. $null = $Command.Parameters.Add($Name, [System.Data.OleDb.OleDbType]::VarWChar, $Size)
  163. $Command.Parameters[$Command.Parameters.Count - 1].Value = $Value
  164. }

Powered by TurnKey Linux.