VB.NET - Mit Lotus Notes E-Mails versenden

Vor etwas mehr als zwei Jahre (wie die Zeit vergeht :-() habe ich hier einen Artikel namens "VB .NET - Mit Lotus Notes E-Mails und Dateianhängen arbeiten" veröffentlicht, der beschreibt, wie man mit Lotus Notes E-Mails aus der Inbox arbeiten kann.

Heute musste ich nun etwas ähnliches bewerkstellen, und zwar E-Mails über unseren Domino Server versenden. Im Prinzip benötigt man dazu keine COM-Verweis auf das Domino Mail Objekt, da man (sofern SMTP auf dem Domino Server freigegegeben ist) das ganze auch schön mit .NET Bordmitteln, sprich Net.Mail.MailMessage usw. lösen kann.

Doch wir wollten auch Faxe über Lotus Notes versenden, und dazu benötigt der Domino Server eine Authentifizierung, was man theoretisch über die .NET Credentials lösen könnte. Leider bekam ich beim Faxversenden immer wieder den Fehler, dass die Benutzerrechte nicht korrekt sein.

Laut Rechereche im Internet hat das auch noch keiner hinbekommen, also blieb mir nichts anderes übrig, als wieder auf das Domino COM-Objekt zu verweisen und damit zu arbeiten.

Herausgekommen dabei sind einige Klassen, mit denen man relativ einfach E-Mails und Faxe über den Server verschicken kann.

Zuerst einmal die Helfer-Klassen:

Hier die Anhangs-Klasse für Dateianhänge:

Public Class LotusNotesMailAttachment
 
  Public FullFilename As String
  Public DisplayName As String
 
  Public Sub New()
  End Sub
 
  Public Sub New(ByVal aFullFilename As String)
    Me.FullFilename = aFullFilename
  End Sub
 
  Public Sub New(ByVal aFullFilename As String, ByVal aDisplayName As String)
    Me.FullFilename = aFullFilename
    Me.Displayname = aDisplayName
  End Sub
 
End Class

Die Liste der Anhänge:

Public Class LotusNotesMailAttachmentList
 
  Inherits List(Of LotusNotesMailAttachment)
 
End Class

Die Klase LotusNotesMailData enthält einfach nur Absender, Empfänger, Betreff usw.
Diese Daten werden später an die Factory zum Versenden weitergereicht.

Public Class LotusNotesMailData
 
  Public Subject As String
 
  Private _To As List(Of String)
  Public Property [To]() As List(Of String)
    Get
      If _To Is Nothing Then
        _To = New List(Of String)
      End If
      Return _To
    End Get
    Set(ByVal value As List(Of String))
      _To = value
    End Set
  End Property
 
  Public From As String
 
  Private _CC As List(Of String)
  Public Property CC() As List(Of String)
    Get
      If _CC Is Nothing Then
        _CC = New List(Of String)
      End If
      Return _CC
    End Get
    Set(ByVal value As List(Of String))
      _CC = value
    End Set
  End Property
  Public Body As String
 
  Private _Attachments As LotusNotesMailAttachmentList
  Public Property Attachments() As LotusNotesMailAttachmentList
    Get
      If _Attachments Is Nothing Then
        _Attachments = New LotusNotesMailAttachmentList
      End If
      Return _Attachments
    End Get
    Set(ByVal value As LotusNotesMailAttachmentList)
      _Attachments = value
    End Set
  End Property
 
End Class

Die folgende Klasse "MailAccount" beinhaltet einfach die "Login-Daten" für den Domino-Server und Infos über das Postfach, das zum Senden verwendet werden soll.

Klasse Mailaccount:

Public Class MailAccount
 
  ''' <summary>
  ''' Hostname of the Domino Server
  ''' </summary>
  ''' <remarks></remarks>
  Public Host As String
 
  ''' <summary>
  ''' Filename with Path of the Notes Mail File
  ''' </summary>
  ''' <remarks>e.g. 'mail\user.nsf'</remarks>
  Public DatabaseFilename As String
 
  Public Password As String
 
  Public Sub New()
  End Sub
 
  Public Sub New(ByVal aHost As String, ByVal aDatabaseFilename As String, ByVal aPassword As String)
    Me.Host = aHost
    Me.DatabaseFilename = aDatabaseFilename
    Me.Password = aPassword
  End Sub
 
End Class

Hier nun meine Notes Factory, die das eigentliche Versenden der E-Mails und Faxe bewerkstelligt. Faxe werden dabei wie E-Mails gehandhabt. Bei unserer Fax-Software für Domino (Ferrari-Fax (übrigens sehr zu empfehlen)) wird statt der E-Mail Adresse einfach die Faxnummer mit dem Zusatz "@Fax" angegeben. Die Faxsoftware wandelt daraufhin alle Anhänge in TIFF um und sendet diese als Fax an den/die Empfänger.

Man erhält zudem das versendete Fax in Form einer E-Mail im "Gesendet" Ordner von Lotus Notes.

Public Class LotusNotesMailFactory
 
  Public Shared Function SendMail(ByVal aMailAccount As MailAccount, ByVal aMaildata As LotusNotesMailData) As Boolean
 
    ' Die Session starten
    Dim Session As New Domino.NotesSession
    Session.Initialize(aMailAccount.Password)
 
    ' Datenbank öffnen
    Dim MailDB As Domino.NotesDatabase
    Dim MailServer As String = Session.GetEnvironmentString("MailServer", True)
    MailDB = Session.GetDatabase(MailServer, aMailAccount.DatabaseFilename)
 
    If Not MailDB.IsOpen Then
      MailDB.Open()
    End If
 
    Dim MailDoc As Domino.NotesDocument = MailDB.CreateDocument
    Dim Body As Domino.NotesRichTextItem = MailDoc.CreateRichTextItem("Body")
 
    MailDoc.ReplaceItemValue("Form", "Memo")
 
    For Each toadr As String In aMaildata.To
      MailDoc.ReplaceItemValue("SendTo", toadr)
      'MailDoc.AppendItemValue("SendTo", aMaildata.To)
    Next
 
    MailDoc.ReplaceItemValue("Subject", aMaildata.Subject)
    'MailDoc.AppendItemValue("Subject", aMaildata.Subject)

    ' CC (Carbon Copy)
    For Each ccadr As String In aMaildata.CC
      MailDoc.AppendItemValue("CopyTo", ccadr)
    Next
 
    ' Create and set the Body content
    Body.AppendText(String.Empty)
    Body.AppendText(aMaildata.Body)
 
    ' Attachment
    For Each lnma As LotusNotesMailAttachment In aMaildata.Attachments
      Body.AddNewLine(2)
      Body.EmbedObject(1454, String.Empty, lnma.FullFilename, lnma.DisplayName)
    Next
 
    ' Example to save the message (optional)
    MailDoc.SaveMessageOnSend = True
 
    ' Send the document
    ' Gets the mail to appear in the Sent items folder
    MailDoc.ReplaceItemValue("PostedDate", Now)
    MailDoc.Send(False)
 
    ' Clean Up
    MailDB = Nothing
    MailDoc = Nothing
    Body = Nothing
    Session = Nothing
 
    Return True
 
  End Function
 
End Class

Wer Verbesserungen hat, oder gar weiß, wie man über die .NET Bordmittel das ganze bewerkstellingen kann, ist herzlich eingeladen, zu kommentieren.

Sollte ich dazukommen, werde ich das ganze noch etwas besser dokumentieren, aufräumen und verbessern.

VCard / VCF Dateien in VB.NET erstellen

Hier eine kleine Klasse, die VCard-Dateien erstellen kann.
Wie man die Klasse aufruft, zeige ich am Ende dieses Eintrags.

Klasse VCard.vb:

Public Class VCard
 
  Private _VCardData As List(Of String)
  Public Property VCardData() As List(Of String)
    Get
      If _VCardData Is Nothing Then
        _VCardData = New List(Of String)
      End If
      Return _VCardData
    End Get
    Set(ByVal value As List(Of String))
      _VCardData = value
    End Set
  End Property
 
  Public InternalName As String
 
  Public VORNAME As String
  Public NACHNAME As String
  Public STRASSE As String
  Public ORT As String
  Public PLZ As String
  Public LAND As String
  Public TELEFONNUMMER As String
  Public TELEFAXNUMMER As String
  Public MOBILNUMMER As String
  Public MAILADRESSE As String
 
  Public Sub New(ByVal aInternalName As String)
 
    Me.InternalName = aInternalName
 
    Me.VCardData.Add("BEGIN:VCARD")
    Me.VCardData.Add("VERSION:2.1")
    Me.VCardData.Add("FN:%VORNAME% %NACHNAME%")
    Me.VCardData.Add("N:%NACHNAME%;%VORNAME%")
    Me.VCardData.Add("ADR;HOME:;;%STRASSE%;%ORT%;;%PLZ%;%LAND%")
    Me.VCardData.Add("TEL;HOME;VOICE:%TELEFONNUMMER%")
    Me.VCardData.Add("TEL;CELL:%MOBILNUMMER%")
    Me.VCardData.Add("TEL;HOME;FAX:%TELEFAXNUMMER%")
    Me.VCardData.Add("EMAIL;WORK;PREF;INTERNET:%MAILADRESSE%")
    Me.VCardData.Add("END:VCARD")
 
  End Sub
 
  ''' <summary>
  ''' Anhand der Felder werden sämtliche Informationen in VCardData durch die jeweiligen Feldwerte ersetzt
  ''' </summary>
  ''' <remarks></remarks>
  Public Sub Fill()
 
    Dim fis As System.Reflection.FieldInfo() = Me.GetType().GetFields()
 
    Dim strReplace As String = String.Empty
    Dim strSearch As String = String.Empty
 
    ' Durch jede Zeile
    For i As Integer = 0 To Me.VCardData.Count - 1
 
      ' Zeile merken
      Dim line As String = Me.VCardData(i)
      If Not line.Contains("%") Then GoTo NextLine
 
      ' Durch alle Felder
      For Each fi As System.Reflection.FieldInfo In fis
 
        strSearch = "%" & fi.Name.ToUpper & "%"
        strReplace = fi.GetValue(Me)
 
        ' Debug.Print("Replacing: " & line & " with: " & strSearch & " - " & strReplace)

        line = Replace(line, strSearch, strReplace)
 
      Next
 
      Me.VCardData(i) = line
 
NextLine:
    Next
 
  End Sub
 
  Public Sub ExportToPath(ByVal aDirectory As String)
 
    ' Mich mit Daten füllen
    Me.Fill()
 
    ' Text für VCard Datei generieren
    Dim strVCardData As String = String.Empty
    For Each line As String In Me.VCardData
      If strVCardData = String.Empty Then
        strVCardData = line
      Else
        strVCardData += vbNewLine & line
      End If
 
    Next
 
    ' Datei schreiben
    My.Computer.FileSystem.WriteAllText(aDirectory & "\" & Me.InternalName & ".vcf", strVCardData, False, System.Text.Encoding.ASCII)
 
  End Sub
 
End Class

Klasse VCardList.vb:

Public Class VCardList
 
  Inherits List(Of VCard)
 
End Class

Aufruf:

Public Class SCVCardCreator
 
  Public Sub New()
  End Sub
 
  Private Function GetList() As VCardList
 
    Dim Result As New VCardList
 
    Using con As New SqlClient.SqlConnection(My.Settings.SQLConnectionString)
 
      Try
        con.Open()
      Catch ex As Exception
        Return Nothing
      End Try
 
      Dim strSQL As String = "Select * From Datenquelle"
 
      Using cmd As New SqlClient.SqlCommand(strSQL, con)
 
        Using dr As SqlClient.SqlDataReader = cmd.ExecuteReader
 
          Do While dr.Read
 
            Try
 
              Dim vcard As New VCard(dr.Item("MAB_NACHNAME"))
              With vcard
 
                Try
                  .VORNAME = dr.Item("MAB_VORNAME")
                Catch ex As Exception
                End Try
 
                Try
                  .NACHNAME = dr.Item("MAB_NACHNAME")
                Catch ex As Exception
                End Try
 
             End With
 
              Result.Add(vcard)
 
            Catch ex As Exception
 
            End Try
 
          Loop
 
        End Using
 
      End Using
 
      con.Close()
    End Using
 
    Return Result
 
  End Function
 
  Public Sub ExportToPath(ByVal aDirectory As String)
 
    Dim lst As VCardList = Me.GetList
 
    For Each vcard As VCard In lst
      vcard.ExportToPath(aDirectory)
    Next
 
  End Sub
 
End Class

Countdown Version 1.1.3826

Auf eine Anfrage hin habe ich Countdown um eine neue Zeiteingabeart erweitert. Bisher konnte man im sogenannten Countdown-Modus ein Enddatum wie beispielsweise die heutige Nacht 24:00 Uhr einstellen.

Nun ist es auch möglich, die gewünschte Endzeit direkt in Tagen, Stunden, Minuten oder Sekunden einzugeben (ich nenne das mal Hochzählen oder CountUp :-).

Die neue Version von Countdown findet sich auf der Programmseite.

MySQL unter VB .NET

Um MySQL unter VB .NET zu verwenden genügt es eigentlich, sich den Quellcode des MySQL Connectors von MySQL.de herunterzuladen, das ganze zu kompilieren und anschließend die kompilierte DLL ins gewünschte Projekt als Referenz einzubinden.

Anschließend kann man wie unter ADO.NET mit Connections, Commands und DataReadern arbeiten:

(angenommen, wir hätten eine Tabelle namens Users, mit einer Spalte Username, die einige Benutzernamen enthält)

Module modMain
 
  Sub Main()
 
    Using con As New MySql.Data.MySqlClient.MySqlConnection("Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword")
 
      Try
        con.Open()
      Catch ex As Exception
        MsgBox(ex.Message, MsgBoxStyle.Critical)
      End Try
 
      Using cmd As New MySql.Data.MySqlClient.MySqlCommand("SELECT * FROM Users", con)
 
        Using dr As MySql.Data.MySqlClient.MySqlDataReader = cmd.ExecuteReader
 
          Do While dr.Read
            MsgBox(dr.Item("Username"))
          Loop
 
        End Using
 
      End Using
 
    End Using
 
  End Sub
 
End Module

Countdown Programm aktualisiert

Da das Jahr sich ja langsam aber sicher dem Ende neigt, ich Urlaub und zudem ein kleines Projekt zum Überarbeiten gesucht habe, bot es sich irgendwie an, Countdown zu aktualisieren.

  • Neu: Neue Aktion "Schriftfarbe zufällig ändern"
  • Neu: Anzeigeformat der Restzeit konfigurierbar
  • Neu: Schrift lässt sich auf Wunsch frei im Fenster positionieren. Hierzu mit Strg-Linksklick das Label positionieren oder mit der Tastatur via (Strg) und hoch/runter links/rechts
  • Neu: Interne Programmprotokollierung mit NLog
  • Verbessert: Alle Aktionen in Threads ausgelagert
  • Verbessert: Vollbild Modus überdeckt nun in allen Windows Versionen die Taskleiste korrekt und kann mit Escape geschlossen werden.
  • Verbessert: Korrekte Positionierung des Textes beim Ändern der Fenstergröße
  • Verbessert: Shortcuts für Notebooks ohne Ziffernblock angepasst
  • Verbessert: Kontextmenü aufgeräumt
  • Änderung: Modifikator-Taste geändert von Umschalt auf Steuerung.
  • Weitere kleinere Änderungen

Die neue Version von Countdown findet sich auf der Programmseite.

Mit NLog unter .NET protokollieren

Wer kennt das nicht? Mal eben ein kleines Programm geschrieben, an die Anwender verteilt und auf irgendeinem Rechner läuft das Programm nicht wie es soll.

Nun ist guter Rat teuer, denn welcher Programmierer gibt sich schon die Mühe in kleinere Tools eine ausgefeilte Protokoll-Funktion zu implementieren um dem Fehler gegebenenfalls schnell auf die Schliche zu kommen?

Mit NLog ist das anders. NLog ist ein Framework fürs Protokollieren unter .NET. Ist der Verweis zu NLog im Projekt erst einmal eingebunden, kann man mit einer Zeile wie

Me.Logger.Warn(ex.Message)

eine Exception inklusive aufrufender Methode und kompletter Exception-Fehlerausgabe protokollieren.

Auf der NLog Webseite ist zwar relativ einfach beschrieben, wie man das Framework in das eigene Projekt einbaut, was ich hier im folgenden allerdings beschreibe, ist noch etwas kürzer, da ausschließlich für VB .NET.

Verweis einbinden

Bevor man das NLog Framework verwenden kann, muss man als Verweis die NLog.dll zum Projekt hinzufügen. Hierzu lädt man auf der Codeplex Projekteseite von NLog am einfachsten die vorkompilierte DLL herunter (man kann sich natürlich auch den Quellcode schnappen und selbst kompilieren :-).

Konfigurationsdatei erstellen

Es gibt zwar viele Varianten, wie NLog nach einer Konfigurationsdatei sucht, aber die einfachste ist, eine Datei namens "nlog.config" im Programmverzeichnis zu erstellen.

Als Inhalt dieser Datei empfehle ich für Anfänger folgendes:

<geshi lang="xml"><nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 
  <targets> 
    <target name="file" xsi:type="File" 
            layout="${longdate} ${logger} ${callsite} ${message} ${exception:Format=tostring} ${newline}" 
            fileName="${basedir}/${windows-identity:domain=false}.log"/> 
  </targets> 
  <rules> 
    <logger name="*" minlevel="Debug" writeTo="file"/> 
  </rules> 
</nlog>

In der Konfigurationsdatei wird mit dem Tag "Target" bestimmt, wohin der Protokolleintrag wandern soll.

Das Target vom Typ "file" beispielsweise schreibt alle Einträge in eine Datei. Außer der Möglichkeit Protokolleinträge in Dateien zu schreiben, bietet NLog dem Benutzer auch eine Mail, ein Textcontrol, ein Netzwerk, das Windows Ereignisprotokoll einen Webservice und eine per ADO.NET ansprechbare Datenbank als Ziel an. Darüberhinaus ist es auch noch möglich, sich eigene Targets, wie beispielsweise einen FTP-Server, zu schreiben. Und natürlich kann man auch mehrere Targets gleichzeitig befüllen.

Im obigen Beispiel wir als Ziel eine Datei verwendet, deren Dateiname aus dem Windows-Benutzername besteht. Die Datei wird dabei automatisch in Unterordnern gelagert, deren Name aus dem aktuellen Datum generiert wird.

Der Tag "Layout" beschreibt dabei, wie die Protokolleinträge formatiert bzw. ergänzt werden.

Der Platzhalter "${longdate}" wird durch das Datum, der Platzhalter "${logger}" durch den Namen der Logging-Klasse, "${callsite}" durch die aufrufende Methode und schließlich "${message}" als die eigentliche Protokollmeldung ersetzt.

Der Zusatz "${exception:Format=tostring}" regelt, dass Exceptions inklusive der Detailmeldungen erfasst werden. Der Platzhalter "${newline}" schließlich, fügt einen Zeilenumbruch am Ende der Meldung ein.

Verwendung

Im Programm selbst, greift man auf NLog beispielsweise so zu:

Public Logger as NLog.Logger = NLog.LogManager.GetCurrentClassLogger
...
Me.Logger.Info("Dies ist ein Test")
Me.Logger.Warn(ex.Message)

Ergänzung vom 21.01.2011:

Hier noch ein Beispiel für eine ziemlich umfassende Protokollausgabe, inklusiv Ausgabe auf die Konsolenanwendung:

<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 
	<targets>
		<target name="file" xsi:type="File" layout="${longdate} ${logger} ${callsite} ${message} ${exception:Format=tostring}" fileName="${basedir}/${windows-identity:domain=false}.log"/>
		<target name="console" xsi:type="Console" layout="${longdate} ${logger} ${callsite} ${message} ${exception:Format=tostring}"/>
	</targets>
 
	<rules>
		<logger name="*" minlevel="Debug" writeTo="file"/>
		<logger name="*" minlevel="Info" writeTo="console"/>	
	</rules>
 
</nlog>

Ergänzung vom 17.02.2011:

Noch ein Beispiel mit einem Formular-Target:

<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 
	<targets>
		<target xsi:type="FormControl" name="FormControlTarget" layout="${longdate} ${logger} ${message} ${exception:Format=tostring}" append="Boolean" controlName="txtLog" formName="frmMain" />
	</targets>
 
	<rules>
		<logger name="*" minlevel="Debug" writeTo="FormControlTarget"/>	
	</rules>
 
</nlog>

VB .NET: MSI Pakete automatisch installieren

Dieses kleine Modul installiert alle MSI-Pakete, die im aktuellen Programmverzeichnis gefunden werden im "Quiet"-Modus, d. h. ohne irgendwelche Nachfragen.

Module modMain
 
  Sub Main()
    Console.WriteLine("Installiere MSI Pakete...")
    Try
      RunAll()
    Catch ex As Exception
      Console.WriteLine("Fehler: " & ex.Message)
      Console.WriteLine("Bitte drücken Sie eine beliebige Taste zum Beenden.")
      Console.ReadLine()
    End Try
    Console.WriteLine("Installation abgeschlossen.")
  End Sub
 
  ''' <summary>
  ''' Nach MSI-Paketen im aktuellen Verzeichnis (dieser EXE) suchen und alle MSI-Pakete
  ''' nacheinander ausführen.
  ''' </summary>
  ''' <remarks></remarks>
  Public Sub RunAll()
 
    Dim lstFiles As Collections.ObjectModel.ReadOnlyCollection(Of String) = _
    My.Computer.FileSystem.GetFiles(Environment.CurrentDirectory, FileIO.SearchOption.SearchTopLevelOnly, "*.MSI")
 
    For Each fn As String In lstFiles
      ExecuteAndWait(fn, "/quiet")
    Next
 
  End Sub
 
  ''' <summary>
  ''' Einen Prozess starten und auf dessen Ende warten
  ''' </summary>
  ''' <param name="ProcessPath">Name der Datei die ausgeführt werden soll</param>
  ''' <param name="Arguments">Optionale Parameter</param>
  ''' <remarks></remarks>
  Public Sub ExecuteAndWait(ByVal ProcessPath As String, Optional ByVal Arguments As String = "")
 
    Dim proc As System.Diagnostics.Process
 
    Try
      proc = New System.Diagnostics.Process()
      proc.StartInfo.FileName = ProcessPath
      proc.StartInfo.Arguments = Arguments
      proc.StartInfo.WindowStyle = ProcessWindowStyle.Normal
      proc.Start()
      proc.WaitForExit()
      proc.Close()
    Catch ex As Exception
      MsgBox(String.Format("Prozess {0} konnte nicht gestartet werden. Fehler: {1}.", ProcessPath, ex.Message))
    End Try
 
  End Sub
 
End Module