Outlook MailItem Save/SaveAs - c#

I have an outlook add-in that allows the user to save an email into a database. When the user does save the email I modify the email subject so it can be identified as being saved.
Saving the email can happen in two ways. Via a button on the tool bar which allows the user to save any email they want, and also via a prompt which appears when a new email is put into the Sent Items folder. Both methods use the same form to save the email!
OK, now to the problem ....
In the process of saving the email I use the mailItem.SaveAs method to put it into the file store. After this has completed successfully i want to change the subject of the email which still exists in outlook to say that it has been saved successfully. I do this by changing myItem.Subject and then using the mailItem.Save method to save the change.
The above works perfectly when the email isn't being saved via the prompt method. So when the user tries to save the email after they send it the mailItem.Save method does not work.
I have narrowed it down to it actually working if i put the myItem.Save() line before the myItem.SaveAs() line, but obviously if I do this I can not guarantee the email was actually saved properly.
So does any one know of a reason that the mailItem.Save method would want to not work after the mailItem.SaveAs method as been called?
Thank you in advance to any suggestions to what might be the problem.
EDIT : Code
if (_item is Outlook.MailItem) { // if the incoming item is an Outlook mail Item
// cast as a mail item
Outlook.MailItem myItem = (Outlook.MailItem)_item;
if (directoryExists(directoryTemp)) { // if the temporary directory exists
bool _profiled = true;
// copy the item as type .msg in the temporary location
myItem.SaveAs(saveTemp, Outlook.OlSaveAsType.olMSG);
// setup impersonation to copy the file to a secure location
PImpersonateUser _iU = new PImpersonateUser();
// do impersonation
try {
_iU.Impersonate("******", "******", "******");
if (File.Exists(savefile)) { // if file already exists in the location
// delete existing file
File.Delete(savefile);
}
// move the temporary file to the secure location with the proper name
File.Move(saveTemp, savefile);
string year = "";
if (ipt_year.SelectedItem != null) { // else if year has been selected
year = ipt_year.SelectedItem.ToString();
}
_profile.profileEmail(folderString(_subject_), _fileName, year);
} catch (Exception e) {
_profiled = false;
// if impersonation fails cancel the impersonation
_iU.Undo();
// show error
MessageBox.Show(e.Source + "\n\n" + e.Message + "\n\n" + e.StackTrace, "SaveAs() Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
} finally {
_iU.Undo();
}
if (_profiled) { // if the email was profiled successfully
// mark the original email as being profiled
markAsProfiled();
}
} else {
// if temporary file save fails throw error
MessageBox.Show("Temporary Directory (" + directoryTemp + ") Does Not Exist!", "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
and the markAsProfiled function ...
private void markAsProfiled() {
if (_item is Outlook.MailItem) { // if the incoming item is an Outlook mail Item
// cast as a mail item
Outlook.MailItem myItem = (Outlook.MailItem)_item;
// make sure subject doesnt already have a profiled flag in the subject
_subject_ = _subject_.Replace("[PROFILED] - ", "");
// add a profiled flag in the subject of the email
myItem.Subject = "[PROFILED] - " + _subject_;
// add a yellow flag to the email
myItem.FlagIcon = Microsoft.Office.Interop.Outlook.OlFlagIcon.olYellowFlagIcon;
// save email with changes made
myItem.Save();
//MessageBox.Show("Mark as Profiled :: " + myItem.Subject + " :: " + myItem.Saved.ToString() + " :: ");
}
}

if this is still relevant to you: You could use a self-defined column in which you could write wether the saving was successfull or not.
Example code:
mail.UserProperties.Add("Profiled", Outlook.OlUserPropertyType.olText, true);
mail.UserProperties["Profiled"].Value = "Yes";
mail.Save();
The only disadvantage is that you've to add the field to the displayed columns in Outlook. (maybe that can be done programmatically)
About why your method doesn't work: I could imagine that Outlook doesn't like it when you change the subject of an email after it was sent.

Related

How to open outlook mail object as a draft email before sending?

void send_reply(Outlook.MailItem item, HashSet<string> names)
{
Outlook.MailItem eMail = item.Reply();
// want to open an email draft box for user to type in the email's content then return to the program here
eMail.Display();
foreach (string s in names)
{
eMail.To = s;
//MessageBox.Show("this is the guy we are sending to " + item.To);
eMail.Importance = Outlook.OlImportance.olImportanceHigh;
((Outlook._MailItem)eMail).Send();
}
}
Want to send a reply to a given mailitem but only to the email addresses specified in names. Issue I'm having is when I call eMail.Display() it only shows for like half a second at most then the draft auto closes and I send a blank reply email to everyone in names.
Anyone have any suggestions?
The Display() function returns immediately and makes your message to be sent empty.
You can wait by passing true to the function:
//...
Outlook.MailItem eMail = item.Reply();
eMail.Display(true); // <-- here
//...
This will make the window Modal and will wait for user to close it.
Maybe you have also to check if the user closed it without a text inside or have the intention to undo the operation...
To do this maybe you can check the message status or register a handler to one (or both) of Close (and Send) events.

How to save only Excel attachments from outlook in C#?

I have to create a program which saves Excel attachments from the mail inbox.
At the moment I am saving all attachments from incoming mails via an event handler, but it seems like that the event is not always triggered but rather 3 from 4 mails only. I don't know the reason though.
So I was thinking about looping through the inbox mails, look for mails with specific subject title and save the attached Excel files.
But how can I do that? Other solutions shows only via add in, but I want to use a Windows service for that.
So far my code (this doesn't work every time though, maybe someone knows a reason for that?)
public partial class MyService : ServiceBase
{
public string AttachPath = #"[mypath to save attachments]";
public MyService()
{
InitializeComponent();
}
public void RunAsConsole(string[] args)
{
Console.WriteLine("This service is executed as a console application.");
Console.WriteLine("Application active.");
OnStart(args);
Console.WriteLine("Press q to exit.");
string userInput = Console.ReadLine();
while (userInput != "q")
{
userInput = Console.ReadLine();
}
Console.WriteLine("Finished! \nPress any key to exit...");
Console.ReadLine();
OnStop();
}
protected override void OnStart(string[] args)
{
Outlook.NameSpace outlookNameSpace;
Outlook.MAPIFolder inbox;
Outlook.Items items;
Outlook.Application oApp = new Outlook.Application();
outlookNameSpace = oApp.GetNamespace("MAPI");
inbox = outlookNameSpace.GetDefaultFolder(Microsoft.Office.Interop.Outlook.OlDefaultFolders.olFolderInbox);
items = inbox.Items;
items.ItemAdd +=
new Outlook.ItemsEvents_ItemAddEventHandler(items_ItemAdd);
}
void items_ItemAdd(object Item)
{
string filter = "[myFilter]";
Outlook.MailItem mail = (Outlook.MailItem)Item;
if (Item != null)
{
if (mail.Subject.ToUpper().Contains(filter.ToUpper()))
{
Console.WriteLine(DateTime.Now.ToShortTimeString() + " Mail found!: " + mail.Subject);
if (mail.Attachments.Count > 0)
{
for (int i = 1; i - 1 < mail.Attachments.Count; ++i)
{
Console.WriteLine($#"Saving {mail.Attachments[i].FileName}");
//Console.WriteLine(Path.Combine(AttachPath, mail.Attachments[i].FileName));
string filepath = Path.Combine(AttachPath, mail.Attachments[i].FileName);
mail.Attachments[i].SaveAsFile(filepath);
//if (File.Exists(filepath))
//{
// mail.Delete(); //after saving the file delete the mail
//}
}
}
else
{
Console.WriteLine("No attachments found: execute auto reply...");
Outlook.MailItem replyMail = mail.Reply();
replyMail.HTMLBody = $#"Some answer for reply";
replyMail.Send();
}
Console.WriteLine("Delete mail: " + mail.Subject.ToString());
mail.UnRead = false; //mark as read
mail.Delete();
}
}
}
protected override void OnStop()
{
//nothing
}
}
At the moment, the service can be executed as a console application and a Windows service, so please don't pay too much attention at that point, it's for debugging reasons.
Other Solutions shows only via add in, but I want to use a windows service for that.
Microsoft does not currently recommend, and does not support, Automation of Microsoft Office applications from any unattended, non-interactive client application or component (including ASP, ASP.NET, DCOM, and NT Services), because Office may exhibit unstable behavior and/or deadlock when Office is run in this environment.
If you are building a solution that runs in a server-side context, you should try to use components that have been made safe for unattended execution. Or, you should try to find alternatives that allow at least part of the code to run client-side. If you use an Office application from a server-side solution, the application will lack many of the necessary capabilities to run successfully. Additionally, you will be taking risks with the stability of your overall solution. Read more about that in the Considerations for server-side Automation of Office article.
As a workaround, you may consider using a low-level API on which Outlook is based on - Extended MAPI or just any wrappers around that API such as Redemption.
If you deal with Exchange only, you may consider using Graph API or EWS, see Start using web services in Exchange for more information.
Please refer to Eugene's answer on the architecture side of things. But from the perspective of extracting the emails with a filter, you can try this code.
Instead of getting items = inbox.Items, try finding the emails with a filter query which returns an Outlook.Table
Then you can iterate on this table to get the emails.
const string PropTag = "http://schemas.microsoft.com/mapi/proptag/";
var filter = "#SQL=" + "\"" + PropTag
+ "0x0037001E" + "\"" + " ci_phrasematch " + "\'" + strFilter + "\'";
Outlook.Table table = inbox.GetTable(filter, Outlook.OlTableContents.olUserItems);
while (!table.EndOfTable)
{
Outlook.Row nextRow = table.GetNextRow();
try
{
Outlook.MailItem mi;
try
{
string entryId = nextRow["EntryID"];
var item = outlookNameSpace.GetItemFromID(entryId);
mi = (Outlook.MailItem)item;
}
catch (InvalidCastException ex)
{
Console.WriteLine("Cannot cast mail item, so skipping. Error: {0}", e);
continue;
}
//Extract the attachments here and archive or reply - put your logic here
}
catch (Exception e)
{
Console.WriteLine("An error occurred: '{0}'", e);
}
}

Addin outlook - Save mail automatically does not work with standard user

I have an outlook addin in which I update programmatically user properties.
Until now, I executed the addin and outlook with administrator rights. Everything was working good.
I attach the event on a forwarded mail :
void Inspectors_NewInspector(Microsoft.Office.Interop.Outlook.Inspector Inspector)
{
try
{
MailItem mail = Inspector.CurrentItem as MailItem;
if (mail != null)
{
((Outlook.ItemEvents_10_Event)mail).Forward += new ItemEvents_10_ForwardEventHandler(Forward_Event);
}
finally
{
Marshal.ReleaseComObject(Inspector);
}
}
Then I update the user properties of the mail and save it.
void Forward_Event(object Forward, ref bool Cancel)
{
MailItem mail = Forward as MailItem;
if (mail != null && OutlookHelper.GetUserProperty(mail) != null)
{
OutlookHelper.SetUserProperty(mail, null);
EventLog.WriteEntry("Application", "Before save forward : " + mail.Saved); // mail.Saved = false
mail.Save();
EventLog.WriteEntry("Application", "After save forward : " + mail.Saved); // mail.Saved = true
}
}
I can see in the logs that the property Saved of my mail is set to True after the call to the Save() method.
However, since I use a standard user to execute the addin (linked to customer needs), when I close the email, a popup appears asking me to save manually the user properties of the forwarded mail. The problem does not appear with admin rights.
Thanks to everybody for help
Well, after some intensive researches, the mail is "read only" and then the call to the Save() method does not really save. When I removed the call to the save, the popup disappeared... I do not really understand this behavior, but it works !
Hope this will help some other people.

faulty login system, will not allow logins

ok, i got it sorted thanks to LordALMMa, but now i have another problem. I want to determine if the user clicks Admin or User radiobutton when registering. I think i should append it to the end of the line on the text file where the name and password is, but how would i do it? Here is the relevant code:
Radio Button Check
public bool radioButtons()
{
string usertypebutton;
if (!userButton.Checked && !adminButton.Checked)
{
MessageBox.Show("You must select an account type");
return false;
}
else
{
if (userButton.Checked)
{
usertypebutton = "User";
}
else
{
usertypebutton = "Admin";
}
return true;
}
}
Streamwriter for registering:
public void mySW()
{
string path = #"C:\Other\myFile.txt";
string userName = userNameBox.Text;
string password = passwordBox.Text;
string usertype = usertypebutton;
using (StreamWriter writer = new StreamWriter(path, true))
{
writer.WriteLine("Username: {0} Password: {1} Type: {3}" , userName, password, usertype);
// No need to close nor dispose your StreamWriter.
// You're inside a using statement for that!
}
MessageBox.Show("Thanks for registering! \n\nYou may now log in!", "Registration SuccessFul");
Application.OpenForms[0].Show();
this.Close();
}
Logging In:
private void logonButton_Click(object sender, EventArgs e)
{
// Loads your users storage
var users = File.ReadAllLines(#"C:\Other\myFile.txt");
// Creates the line with username + password
var usernamePassword = String.Format("Username: {0} Password: {1}", userNameBox.Text, passwordBox.Text);
// Locates the user on your storage
var userFound = users.SingleOrDefault(_u => _u.Equals(usernamePassword));
if (userFound != null)
{
MessageBox.Show("Welcome back, " + userNameBox.Text);
}
else
{
MessageBox.Show("Sorry, you have entered incorrect details\n\nPlease try again");
userNameBox.Text = "";
passwordBox.Text = "";
}
}
So (I think) essentially i want to pass the value usertypebutton from radiobutton method, to the SW. How would i do it, as i'm already passing a boolean value?
Anthony
One part of the problem is that you are not writing the same string that you're reading:
writer.WriteLine("Password: " + userName + " " + "Password: " + password);
I'm guessing that was a typo in your post... but if not that could be your issue.
The other problem is probably this right here:
using (StreamWriter writer = new StreamWriter(path, true))
If you look up the documentation on that overload of the StreamWriter constructor, you'd see that you specified append = true. You are appending each set of login credentials to a file on its own line. But then later, you are only reading the first line of that file. So you will always read the first set of credentials that were entered when the file was first created.
That aside, I hope you are just doing this as an experiment since it is not a secure way of managing passwords to write them to a file like that. Also, you don't need to call Close and Dispose on a Stream if you wrap it in a using block, so you should stick to doing that.
Anthony, dispite the fact that storing logins this way is a major security problem (it's not even a risk anymore), there are some changes to your code that I'd do.
The issue is that you're not storing "Username: [username] Password: [password]".
If you double-check your saving method, you're storing "Password: [username] Password: [password]". That's why they are never found.
Here follows some changes:
Consider:
public void mySW()
{
string path = #"C:\Other\myFile.txt";
string userName = userNameBox.Text;
string password = passwordBox.Text;
using (StreamWriter writer = new StreamWriter(path, true))
{
// This overload makes your life easier by auto-formatting variables for you.
// Also, avoid the "string1 + string2" concatenation mode.
// Use String.Format instead. It's easier to read and keep over time.
writer.WriteLine("Username: {0} Password: {1}", userName, password);
// No need to close nor dispose your StreamWriter.
// You're inside a using statement for that!
}
MessageBox.Show("Thanks for registering! \n\nYou may now log in!", "Registration SuccessFul");
Application.OpenForms[0].Show();
this.Close();
}
And your other method should look like:
{
// Loads your users storage
var users = File.ReadAllLines(#"C:\Other\myFile.txt");
// Creates the line with username + password
var usernamePassword = String.Format("Username: {0} Password: {1}", userNameBox.Text, passwordBox.Text);
// Locates the user on your storage
// This uses Linq syntax with lambda. Linq without lamba looks similar to SQL.
// Lambda is a bit more advanced but reduces code-size and it's easier to understand (IMHO).
// This code will iterate through users (list of string) and try to retrieve one that's equal to the contents of usernamePassword.
var userFound = users.SingleOrDefault(_u => _u.Equals(usernamePassword));
// If null, indicates that no username/password combination was found.
if (userFound != null)
{
MessageBox.Show("Welcome back, " + userNameBox.Text);
}
else
{
MessageBox.Show("Sorry, you have entered incorrect details\n\nPlease try again");
userNameBox.Text = "";
passwordBox.Text = "";
}
}
I'm not checking for the exception. SingleOrDefault will throw an exception if 2 or more records are found mathing the search pattern.
I'm not checking that because this will increase complexity here with try-catch and also because for that to work properly, I'd have to check if they exits BEFORE recording, so changing the register method.
But I think you've got the idea here.
Have you checked your output files? You are writing Password: X Password: Y:
writer.WriteLine("Password: " + userName + " " + "Password: " + password);
and you are checking Username: X Password: Y
if (user == ("Username: "+userNameBox.Text.Trim()+" "+"Password: "+passwordBox.Text.Trim()))
You are adding line as
writer.WriteLine("Password: " + userName + " " + "Password: " + password);
^1 ^2
^1 must be Username:
There are some points which I cannot pass without pointing:
What would you do if file structure corrupted?
What if a user wants to register twice with same username and password?
Please encode the passwords. This is not ethic. You put at risk your members who uses same account information in somewhere else.
Try using a database which is stronger and faster than a text file.

TFS only check in specific files and folders and not all pending changes

I am still having problems communicating with TFS through my C# application. I am trying to work with PendingChanges to check in files created by my application but after hours of google research and reading I have yet to find a way to ONLY check in specific files. Whenever I do a check in, TFS simply checks in ALL items that are currently checked out PLUS the ones I tell it to check in. Is there a way to remove certain items from the PendingChanges object or create a completely new pendingchanges object with JUST the files I need checked in? This entire behavior of all or nothing seems to be quite ridiculous. Please help.
Workspace myWorkspace = createWorkspace();
// Show our pending changes.
PendingChange[] pendingChanges = myWorkspace.GetPendingChanges();
rt.Text += "Your current pending changes: \n";
foreach (string f in checkinItems)
{
foreach (PendingChange pendingChange in pendingChanges)
{
if (Path.Combine(localPath, f) != pendingChange.LocalItem)
{
toCheckIn.Add(Path.Combine(localPath, f));
rt.Text += "Found one!" + Path.Combine(localPath, f).ToString() + "\n";
break;
}
else
{
rt.Text += pendingChange.LocalItem + " Not ours. \n";
}
}
}
myWorkspace.PendAdd(toCheckIn.ToArray(), true);
// Checkin the items we added.
int changesetNumber = myWorkspace.CheckIn(pendingChanges, currentUserName + ": " + toCheckIn + " from CokomImport");
rt.Text += "Checked in changeset " + changesetNumber;
This is the code I have so far. It filters out the stuff that I don't need but in the end it makes no difference because I need to check in the original PendingChanges with the stuff I need added to it.

Categories

Resources