Brianary

A pedant that hangs out in the dark corner-cases of the web.

Thursday, August 07, 2014

Macro to set Outlook email expiration with subject hashtag

When you return from vacation, you probably have a ton of irrelevant spam for events that happened while you were gone but are no longer useful, like:
  • people leaving early/coming in late
  • lunch/available food
  • server reboots
  • weather/traffic warnings
One way to reduce this noise is to set an expiration on emails of this nature. Sadly, Microsoft Outlook has deeply hidden the UI to do this, so most people don't remember, can't figure it out, or can't be bothered.
Here is a macro that lets you set the expiration date for an email by just adding certain hashtags to your subject. Adding #today sets the expiration for the end of the day, otherwise you can use ISO 8601 durations as hashtags, like #PT2H for two hours, or #P1W for a week.

Public WithEvents Item As Outlook.MailItem

Private Function DateUnit(ByVal isoUnit, ByVal isYmwdPart) As String
    Select Case isoUnit
        Case "Y": DateUnit = "yyyy"
        Case "W": DateUnit = "ww"
        Case "M":
            If isYmwdPart Then
                DateUnit = "m"
            Else
                DateUnit = "n"
            End If
        Case Else: DateUnit = isoUnit
    End Select
End Function

Private Function AddIsoDuration(ByVal start As Date, ByVal isoDuration As String) As Date
    Dim value, durationMatch, nextPart
    value = start
    Set durationMatch = New RegExp
    durationMatch.Pattern = "^(P((\d+[YMWD])*)(T((\d+[HMS])+))?)$"
    Set nextPart = New RegExp
    nextPart.Pattern = "^(\d+)([YMWDHMS])"
    Set matched = durationMatch.Execute(isoDuration)(0)
    ymwd = matched.SubMatches(1)
    hms = matched.SubMatches(4)
    Do Until Len(ymwd) = 0
        Set part = nextPart.Execute(ymwd)(0)
        value = DateAdd(DateUnit(part.SubMatches(1), True), CInt(part.SubMatches(0)), value)
        ymwd = Mid(ymwd, Len(part) + 1)
    Loop
    Do Until Len(hms) = 0
        Set part = nextPart.Execute(hms)(0)
        value = DateAdd(DateUnit(part.SubMatches(1), False), CInt(part.SubMatches(0)), value)
        hms = Mid(hms, Len(part) + 1)
    Loop
    AddIsoDuration = value
End Function

Private Sub Application_ItemSend(ByVal Item As Object, Cancel As Boolean)
    Dim durationMatch
    Set durationMatch = New RegExp
    durationMatch.Pattern = "#(P(\d+[YMWD])*(T(\d+[HMS])+)?)\b"
    ExpiryTime = Item.ExpiryTime
    If InStr(1, Item.Subject, "#today", vbTextCompare) > 0 Then
        If ExpiryTime = #1/1/4501# Then
            Item.ExpiryTime = DateAdd("d", 1, Date)
        End If
    ElseIf durationMatch.Test(Item.Subject) Then
        If ExpiryTime = #1/1/4501# Then
            Item.ExpiryTime = AddIsoDuration(Now, durationMatch.Execute(Item.Subject)(0).SubMatches(0))
        End If
    End If
    Item.Save
End Sub

Wednesday, October 30, 2013

PowerShell script to parse ASP.NET errors from the event log

Here is a script that exhaustively parses out the ASP.NET details from event log entries.

Get-AspNetEvents.ps1

<#
.Synopsis
Parses ASP.NET errors from the event log on the given server.
.Parameter ComputerName
The name of the server on which the error occurred.
.Parameter EntryType
Gets only events with the specified entry type. Valid values are Error, Information, and Warning. The default is all events.
.Parameter After
Skip events older than this datetime.
.Parameter Before
Skip events newer than this datetime.
.Parameter Newest
The maximum number of the most recent events to return.
#>

#requires -version 3
[CmdletBinding()] Param(
[Parameter(Mandatory=$true,Position=0)][Alias('CN','Server')][string[]]$ComputerName,
[ValidateSet('Information','Warning','Error')][string[]]$EntryType,
[DateTime]$After,
[DateTime]$Before,
[int]$Newest
)
$FieldNames= @(
    @('EventCode','EventMessage','EventTime','EventTimeUtc','EventId','EventSequence','EventOccurrence',
        'EventDetailCode','AppDomain','TrustLevel','AppPath','AppLocalPath','MachineName','_','ProcessId','ProcessName',
        'AccountName','ExceptionType','ExceptionMessage','RequestUrl','RequestPath','UserHostAddress','User','IsAuthenticated',
        'AuthenticationType','ReqThreadAccountName','ThreadId','ThreadAccountName','IsImpersonating','StackTrace','CustomEventDetails'),
    @('EventCode','EventMessage','EventTime','EventTimeUtc','EventId','EventSequence','EventOccurrence',
        'EventDetailCode','AppDomain','TrustLevel','AppPath','AppLocalPath','MachineName','_','ProcessId','ProcessName',
        'AccountName','RequestUrl','RequestPath','UserHostAddress','User','IsAuthenticated','AuthenticationType',
        'ThreadAccountName','CustomEventDetails')
) |sort Length
$RemoveFields= '_','ThreadAccountName','ReqThreadAccountName' # blank or redundant fields
$BoolFields= 'IsAuthenticated','IsImpersonating'
$IntFields= 'EventOccurrence','EventSequence','EventCode','EventDetailCode','ProcessId','ThreadId'
$EventQuery = @{
    ComputerName = $ComputerName
    LogName      = 'Application'
    Source       = 'ASP.NET 4.0.30319.0','ASP.NET 2.0.50727.0','ASP.NET 1.1.4322.0'
}
if($After){$EventQuery.After=$After}
if($Before){$EventQuery.Before=$Before}
if($Newest){$EventQuery.Newest=$Newest}
if($EntryType){$EventQuery.EntryType=$EntryType}
Get-EventLog @EventQuery |
    ? {1017,1019,1023,1025 -notcontains $_.EventID} | # don't want ASP.NET registration events
    % {
        [string]$type = $_.EntryType
        $fields = @{EntryType=$type;Source=$_.Source;EventTime=$_.TimeGenerated}
        if($type -eq 'Error')
        { # errors aren't structured nicely
            if($_.Message -match '(?m)^Application ID: (?.+)$'){$fields.AppId=$Matches.AppId.TrimEnd()}
            if($_.Message -match '(?m)^Process ID: (?.+)$'){$fields.ProcessId=[int]$Matches.ProcessId.TrimEnd()}
            if($_.Message -match '(?m)^Exception: (\w+\.)*(?\w+)\s*$'){$fields.ExceptionType=$Matches.ExceptionType}
            if($_.Message -match '(?m)^Message: (?.+)$'){$fields.ExceptionMessage=$Matches.ExceptionMessage.TrimEnd()}
            if($_.Message -match '(?ms)^StackTrace: (?.+)$'){$fields.StackTrace=$Matches.StackTrace.TrimEnd()}
        }
        elseif($_.ReplacementStrings.Length)
        {
            $values = $_.ReplacementStrings
            $names = $FieldNames |? Length -ge $values.Length |select -f 1
            if($values.Length -gt $names.Length) { Write-Warning ('Unexpected field values: {0} > {1}' -f $values.Length,$names.Length) }
            for($i=0; $i -lt $values.Length; $i++) {$fields[$names[$i]]= $values[$i].TrimEnd()}
            $RemoveFields |% {$fields.Remove($_)}
            $BoolFields |% {$fields[$_]=[bool]$fields[$_]}
            $IntFields |% {$fields[$_]=[int]$fields[$_]}
            $fields.RequestUrl= [uri]$fields.RequestUrl
            $fields.EventTime= [datetime]::Parse($fields.EventTime,$null,[Globalization.DateTimeStyles]::AssumeLocal)
            $fields.EventTimeUtc= [datetime]::Parse($fields.EventTimeUtc,$null,[Globalization.DateTimeStyles]::AssumeUniversal)
            if($fields.ExceptionMessage -and $fields.StackTrace)
            { $fields.ExceptionMessage= $fields.ExceptionMessage.Replace($fields.StackTrace,'').TrimEnd() } # don't need stack trace twice
        }
        $event = New-Object PSObject -p $fields
        $event.PSObject.TypeNames.Insert(0,'AspNetApplicationEventLogEntry')
        $event
    }

Tuesday, May 07, 2013

Find-Lines.ps1

<#
.Synopsis
Searches files for pattern, displays matches, opens in text editor.
.Parameter Pattern
Specifies the text to find. Type a string or regular expression. 
If you type a string, use the SimpleMatch parameter.
.Parameter Filters
Specifies wildcard filters that files must match one of.
.Parameter Path
Specifies a path to one or more locations. Wildcards are permitted. 
The default location is the current directory (.).
.Parameter Include
Wildcard patterns files must match one of (slower than Filter).
.Parameter Exclude
Wildcard patterns files must not match any of.
.Parameter CaseSensitive
Makes matches case-sensitive. By default, matches are not case-sensitive. 
.Parameter List
Returns only the first match in each input file. 
By default, Select-String returns a MatchInfo object for each match it finds.
.Parameter NotMatch
Finds text that does not match the specified pattern.
.Parameter SimpleMatch
Uses a simple match rather than a regular expression match. 
In a simple match, Select-String searches the input for the text in the Pattern parameter. 
It does not interpret the value of the Pattern parameter as a regular expression statement.
.Parameter NoRecurse
Disables searching subdirectories.
.Example
C:\PS> Find-Lines 'using System;' *.cs "$env:USERPROFILE\Documents\Visual Studio*\Projects" -CaseSensitive -List

This command searches all of the .cs files in the Projects directory (or directories) and subdirectories,
displays the first matching line of each file with a match, then opens the file to the correct line in the editor.
#>
[CmdletBinding()]Param(
  [Parameter(Position=0,Mandatory=$true)][string[]]$Pattern,
  [Parameter(Position=1)][string[]]$Filters,
  [Parameter(Position=2)][string[]]$Path,
  [string[]]$Include,
  [string[]]$Exclude,
  [switch]$CaseSensitive,
  [switch]$List,
  [switch]$NotMatch,
  [switch]$SimpleMatch,
  [switch]$NoRecurse
)
# set up splatting
$lsopt = @{Recurse=!$NoRecurse}
if($Path) { $lsopt.Path=$Path }
if($Include) { $lsopt.Include=$Include }
if($Exclude) { $lsopt.Exclude=$Exclude }
$ssopt = @{'Pattern'=$Pattern}
if($CaseSensitive) { $ssopt.CaseSensitive=$true }
if($List) { $ssopt.List=$true }
if($NotMatch) { $ssopt.NotMatch=$true }
if($SimpleMatch) { $ssopt.SimpleMatch=$true }
# the filter parameter is much faster than the include parameter
Select-String -Path ($( if($Filters) { $Filters|% {ls @lsopt -Filter $_} } else { ls @lsopt } ) |
    ? {Test-Path $_.FullName -PathType Leaf}) @ssopt |
  ogv -p -t "Search: '$Pattern' $Filters" |
  % {emeditor $_.Path /l $_.LineNumber} #TODO: customize editor

Thursday, April 11, 2013

TLS (HTTPS) query strings: encrypted

Wednesday, April 10, 2013

Copy-SchTasks.ps1

A simple PowerShell script to copy scheduled tasks from one machine (often much older) to another.
<#
.Synopsis
Copy scheduled jobs from another computer to this one, using a GUI list to choose jobs.
.Parameter ComputerName
The name of the computer to copy jobs from.
.Parameter DestinationComputerName
The name of the computer to copy jobs to (local computer by default).
#>
[CmdletBinding()]Param(
[Parameter(Mandatory=$true,Position=0)][string]$ComputerName,
[Parameter(Position=1)][Alias('To','Destination')][string]$DestinationComputerName = $env:COMPUTERNAME
)
$TempXml= [io.path]::GetTempFileName()
$CredentialCache = @{}
function Get-CachedCredentials([Parameter(Mandatory=$true,Position=0)][string]$UserName)
{
    if(!$CredentialCache.ContainsKey($UserName))
    { $CredentialCache.Add($UserName,(Get-Credential -Message "Enter credentials for $UserName tasks" -UserName $UserName)) }
    $CredentialCache[$UserName]
}
function ConvertFrom-Credential([Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)]$Credential)
{ $Credential.GetNetworkCredential().Password }
schtasks /query /s $ComputerName /v /fo csv |
    ConvertFrom-Csv |
    ogv -p -t 'Select jobs to copy' |
    select TaskName,'Run As User' -Unique |
    % {
        schtasks /query /s $ComputerName /tn $_.TaskName /xml ONE |Out-File -Encoding unicode $TempXml
        schtasks /create /s $DestinationComputerName /tn $_.TaskName /ru ($_.'Run As User') `
            /rp (Get-CachedCredentials $_.'Run As User' |
            ConvertFrom-Credential) /xml $TempXml
        rm $TempXml
    }
$CredentialCache.Clear()

Wednesday, January 23, 2013

Installing to the GAC



Note
In earlier versions of the .NET Framework, the Shfusion.dll Windows shell extension enabled you to install assemblies by dragging them in File Explorer. Beginning with the .NET Framework 4, Shfusion.dll is obsolete.

Note
Gacutil.exe is only for development purposes and should not be used to install production assemblies into the global assembly cache.

(Also: Yikes, there are multiple GACs (by CLR, and by 32/64-bit processor architecture): .NET 4.0 has a new GAC, why? - Stack Overflow)


Replacement for the alternative: http://wixtoolset.org/ (not http://www.wix.com/) ("Standard Custom Actions"?):

Tuesday, October 02, 2012

The Ideal Wiki

  • A complete, powerful, standard wiki dialect with full HTML5 elements support.
  • Easy to modify wiki-wide CSS.
  • Good media management for uploading/pasting images and other media content.
  • Good table support, including simple native embedded CSV/TSV/PSV/SKV/LOG data, full CALS Table Model support, and numeric/monetary column detection and alignment, leveraging something like accounting.js.
  • Support for article tags (keyword metadata), and dynamically-built lists of articles with a given tag.
  • Built-in icons or symbols, like Font Awesome.
  • Built-in syntax highlighting (like highlight.js).
  • Integration with javascript graphing libraries, or text-description-driven diagramming tools, like:
  • Modularity for additional extensions and libraries.
  • Support for context expiration alerts and requests for review/moderation.
  • Full export and a programmable interface to search/read/add/modify/delete articles.