Reset Outlook PST password programmatically - c#

I am having trouble remembering my Outlook PST password, and cannot import that file into Outlook 2016. I have tried several forums, free password recovery software tools, but could not reset/recover the password. After browsing a lot, I found out Aspose.Email for .NET has a backdoor to access the properties of PST file and there's a way to reset password. I have no idea about this, and have never used .NET or C#. Is there someone who can help me out here?
Following are the related links from my browsing results:
Aspose.Email for .NET - https://dzone.com/articles/how-to-check-set-remove-or-update-password-of-pst
PST Password Property - https://msdn.microsoft.com/en-us/library/ff385916(v=office.12).aspx
Please note my PST is UNICODE and not ANSI format, so following is already out of question.
http://www.itninja.com/blog/view/how-to-unlock-password-protected-pst-file-6
This is what I have tried online #https://dotnetfiddle.net/:
using System;
using Aspose.Email.Mapi;
using Aspose.Email.Storage.Pst;
namespace Aspose.Email.Examples.CSharp.Email.Outlook
{
class RemovingPaswordProperty
{
public static void Run()
{
// The path to the File directory.
// ExStart:RemovingPaswordProperty
string dataDir = RunExamples.GetDataDir_Outlook();
PersonalStorage personalStorage = PersonalStorage.FromFile(dataDir + "PersonalStorage1.pst");
if (personalStorage.Store.Properties.ContainsKey(MapiPropertyTag.PR_PST_PASSWORD))
{
MapiProperty property = new MapiProperty(MapiPropertyTag.PR_PST_PASSWORD, BitConverter.GetBytes((long)0));
personalStorage.Store.SetProperty(property);
}
// ExEnd:RemovingPaswordProperty
}
}
}
and I am getting following errors:
Error(s):
Compilation error (line 14, col 30): The name 'RunExamples' does not exist in the current context

Related

How to upload a file from my local machine to a vault of s3 glacier using c# in a console app?

did someone knows how to do that because i had investigate about, but i found only wrong/don't working answers I had try a lot of solutions but it seems to be wrong, like using the Chilkat directory , using ArchiveTransferManager ...
Chilkat.Rest rest = new Chilkat.Rest();
bool bTls = true;
int port = 443;
bool bAutoReconnect = true;
bool success = rest.Connect("glacier.eu-west-1.amazonaws.com", port, bTls, bAutoReconnect);
Chilkat.AuthAws authAws = new Chilkat.AuthAws();
authAws.AccessKey = ;
authAws.SecretKey = ;
authAws.ServiceName = "glacier";
authAws.Region = "us-west-1";
success = rest.SetAuthAws(authAws);
rest.AddHeader("x-amz-glacier-version", "2012-06-01");
string filePath = "20190422.csv";
Chilkat.Crypt2 crypt = new Chilkat.Crypt2();
crypt.HashAlgorithm = "sha256-tree-hash";
crypt.EncodingMode = "hexlower";
string treeHashHex = crypt.HashFileENC(filePath);
rest.AddHeader("x-amz-sha256-tree-hash", treeHashHex);
crypt.HashAlgorithm = "sha256";
string linearHashHex = crypt.HashFileENC(filePath);
authAws.PrecomputedSha256 = linearHashHex;
rest.AddHeader("x-amz-archive-description", filePath);
Chilkat.Stream fileStream = new Chilkat.Stream();
fileStream.SourceFile = filePath;
string responseStr = rest.FullRequestStream("POST", "/682988997959/vaults/streamqueuesvault", fileStream);
if (rest.LastMethodSuccess != true)
{
Debug.WriteLine(rest.LastErrorText);
return;
}
int respStatusCode = rest.ResponseStatusCode;
if (respStatusCode >= 400)
{
Debug.WriteLine("Response Status Code = " + Convert.ToString(respStatusCode));
Debug.WriteLine("Response Header:");
Debug.WriteLine(rest.ResponseHeader);
Debug.WriteLine("Response Body:");
Debug.WriteLine(responseStr);
return;
}
Debug.WriteLine("response status code = " + Convert.ToString(respStatusCode));
string archiveId = rest.ResponseHdrByName("x-amz-archive-id");
Debug.WriteLine("x-amz-archive-id = " + archiveId);
string location = rest.ResponseHdrByName("Location");
Debug.WriteLine("Location = " + location);
Here is a step by step guide on How to upload a file from my local machine to a vault of s3 glacier using c# in a console app?. First I would like to present some basic background information that will be used later in the solution. Feel free to skip ahead to the solution if you are smart on S3 Glacier.
If you have AWS SDK for .NET and VS already installed, you can download the Repo from Github.
Quick Intro to S3-Glacier
Amazon S3 Glacier is Amazons low cost long term storage service.
In Glacier terminology, an object is referred to as an Archive. Also the folders where you store archives are called Vaults. Its pretty simple - From the Glacier FAQ:
Q: How is data within Amazon S3 Glacier organized?
You store data in Amazon S3 Glacier as an archive. Each archive is assigned a unique archive ID that can later be used to retrieve the data. An archive can represent a single file or you may choose to combine several files to be uploaded as a single archive. You upload archives into vaults. Vaults are collections of archives that you use to organize your data.
When you upload objects to S3 Glacier, the objects don't immediately appear in your Glacier console. Your Glacier console will refresh once a day.
Amazon recommends you use the AWS SDK for .NET when developing C# applications that interface AWS services.
Simple Solution
Before you code, go into your AWS Console and create a S3 Glacier Vault name 'TestVault'.
At the time of this solution (April 2019), I suggest you use Visual Studio 2019. These steps are similar for earlier versions of Visual Studio.
The code I present was taken directly from the AWS SDK for .NET Documentation.
Once your visual studio is ready, then follow these steps:
Create a new project (use template -> Console App (.NET Framework) - not Console App (.NET Core) and name it ConsoleApp9
Add the AWS SDK to your project via NuGet package manager command.
Tools menu, select Nuget Package Manager, and click Package Manager Console.
then type Install-Package AWSSDK.
For a MAC use Project->Add Nuget Packages. Search for "AWSSDK.Glacier" and install it.
Below is the working code. You need to copy most of this into your Program.cs and remove the default "Hello World" code. Your final Program.cs code should look like
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Amazon.Glacier;
using Amazon.Glacier.Transfer;
using Amazon.Runtime;
namespace ConsoleApp9
{
class Program
{
static string vaultName = "TestVault";
static string archiveToUpload = "C:\\Windows\\Temp\\TEST-ARCHIVE.txt";
static void Main(string[] args)
{
try
{
var manager = new ArchiveTransferManager(Amazon.RegionEndpoint.USEast1);
// Upload an archive.
string archiveId = manager.Upload(vaultName, "upload archive test", archiveToUpload).ArchiveId;
Console.WriteLine("Archive ID: (Copy and save this ID for use in other examples.) : {0}", archiveId);
Console.WriteLine("To continue, press Enter");
Console.ReadKey();
}
catch (AmazonGlacierException e) { Console.WriteLine(e.Message); }
catch (AmazonServiceException e) { Console.WriteLine(e.Message); }
catch (Exception e) { Console.WriteLine(e.Message); }
Console.WriteLine("To continue, press Enter");
Console.ReadKey();
}
}
}
Put the file that you want to be uploaded to Glacier as c:\Windows\Temp\Test-Archive.txt. You can put the file anywhere you want, just update the variable archiveToUpload in your code to reflect the location.
If your region is not USEast1, Change the AWS Region on the line just after the try:
var manager = new ArchiveTransferManager(Amazon.RegionEndpoint.YOUR-REGION);
Run the program and it will upload the file. If you have installed the AWS SDK before this will likely work just fine and you will have a screen that shows your archive id.:
If you run into permissions or authorization errors - please follow these steps on setting up authorization for the AWS SDK. I recommend using a Credentials File (2nd option from top). Other problems could be wrong Vault Name or it cant find the file on your machine.
When you go back to the Glacier console, you will not see any files uploaded. Glacier is low cost and slow moving compared to s3 and so your Vault contents are updated once a day.
As long as you get an ID in step 6, your file was successfully stored in Glacier.
Hope this helps and you find success.
Make sure your region is consistent. In the following code, "eu-west-1" is used in the Connect call, but "us-west-1" is used for authAws.Region.
bool success = rest.Connect("glacier.eu-west-1.amazonaws.com", port, bTls, bAutoReconnect);
Chilkat.AuthAws authAws = new Chilkat.AuthAws();
authAws.AccessKey = ;
authAws.SecretKey = ;
authAws.ServiceName = "glacier";
authAws.Region = "us-west-1";

Get Direct Reports from Logged in user from Exchange

I need to get the direct reports from a logged in user (MVC 4)
I don't need the names of the direct reports but I do need their email addresses including their proxy addresses.
So for this reason I need to search through Exchange. I personally have never attempted to search Exchange in the past and everything I find out there tells me how to get from step 8 to the finish line but says nothing about how to go from step 1 to 8.
I can get the current users user name by simply
User.Identity.Name.Replace(#"yourdomain\", "")
and I have found this example which so far is probably the best example I have found
http://msdn.microsoft.com/en-us/library/office/ff184617(v=office.15).aspx
but even with that example the line
Outlook.AddressEntry currentUser =
Application.Session.CurrentUser.AddressEntry;
is not actually getting the current user logged into the site.
I really hope someone out there is familiar with this and can get me past this point.
I reworked the sample from the URL as the following LINQPad 4 query. I've found that LINQPad is a great way to experiment because it is very scripty, allowing quick experimentation, and you can easily view data by using the Dump() extension method. Purchasing intellisense support is totally worthwhile.
Also, I noticed there is a lot of fine print like:
The logged-on user must be online for this method to return an AddressEntries collection; otherwise, GetDirectReports returns a null reference. For production code, you must test for the user being offline by using the _NameSpace.ExchangeConnectionMode property, or the _Account.ExchangeConnectionMode property for multiple Exchange scenarios.
and
If the current user has a manager, GetDirectReports() is called to return an AddressEntries collection that represents the address entries for all the direct reports of user’s manager. If the manager has no direct reports, GetDirectReports returns an AddressEntries collection that has a count of zero.
So there are a lot of assumptions like Exchange is configured properly with Direct Report relationships, and the current user is online...which I believe brings Lync into the equation. Hopefully this LINQPad query will be useful to you. Just copy and paste it into a text editor and name it with the .linq file extension. You'll then be able to open it in LINQPad 4. BTW: You're question caught my attention because there was talk recently at my work of pulling direct reports from Active Directory. I wish I could be more helpful...good luck.
<Query Kind="Program">
<Reference><ProgramFilesX86>\Microsoft Visual Studio 12.0\Visual Studio Tools for Office\PIA\Office15\Microsoft.Office.Interop.Outlook.dll</Reference>
<Reference><ProgramFilesX86>\Microsoft Visual Studio 12.0\Visual Studio Tools for Office\PIA\Office15\Microsoft.Office.Interop.OutlookViewCtl.dll</Reference>
<Namespace>Microsoft.Office.Interop.Outlook</Namespace>
</Query>
void Main()
{
GetManagerDirectReports();
}
// Define other methods and classes here
private void GetManagerDirectReports()
{
var app = new Microsoft.Office.Interop.Outlook.Application();
AddressEntry currentUser = app.Session.CurrentUser.AddressEntry;
if (currentUser.Type == "EX")
{
ExchangeUser manager = currentUser.GetExchangeUser().GetExchangeUserManager();
manager.Dump();
if (manager != null)
{
AddressEntries addrEntries = manager.GetDirectReports();
if (addrEntries != null)
{
foreach (AddressEntry addrEntry in addrEntries)
{
ExchangeUser exchUser = addrEntry.GetExchangeUser();
StringBuilder sb = new StringBuilder();
sb.AppendLine("Name: " + exchUser.Name);
sb.AppendLine("Title: " + exchUser.JobTitle);
sb.AppendLine("Department: " + exchUser.Department);
sb.AppendLine("Location: " + exchUser.OfficeLocation);
sb.Dump();
}
}
}
}
}
I would suggest using EWS Managed API in conjunction with your code to get the direct reports for a user. As Jeremy mentioned in his response that you need to have your direct report relationships already set up. To help you get started, here some steps to get EWS Managed API up and running:
Download the latest version of EWS Managed API
Get started with EWS Managed API client applications to learn about how to reference the assembly, set the service URL, and communicate with EWS.
Start working with your code. If you need some functioning code to get you going, check out the Exchange 2013 101 Code Samples that has some authentication code already written and a bunch of examples you can modify to make your own.
If you have the email address or user name of the current user you can use the ResolveName() method to get to their mailbox to retrieve additional information. Here is an article to help with that method: How to: Resolve ambiguous names by using EWS in Exchange 2013
Essentially you want to get to the point where you can run a command similar to this:
NameResolutionCollection coll = service.ResolveName(NameToResolve, ResolveNameSearchLocation.DirectoryOnly, true, new PropertySet(BasePropertySet.FirstClassProperties));
If you give a unique enough value in the NameToResolve parameter you should only get back one item in the collection. With that, you can look at the direct reports collection within that one item and see not only the names of their direct reports, but their email addresses as well.
I hope this information helps. If this does resolve your problem, please mark the post as answered.
Thanks,
--- Bob ---

Check if there is any kind of PDF Reader installed

I have a Help function in my Application, that consists of one webbrowser control. That webbrowser control gets filled with a .pdf file, the source for that .pdf file is our own website.
The problem is, that not everyone will have a PDF Reader installed on their machine, so I want to check whether one is installed: Yes or No. I searched the internet and I mostly saw that users on Stackoverflow where wanting to check if Adobe Reader was installed, that is not what I want. I need to know IF there is a PDF Reader somewhere installed on the machine.
I did find the following code, that can possibly help me:
public void CheckPdfReaderAvailable()
{
RegistryKey key = Registry.ClassesRoot.OpenSubKey(".pdf");
Assert.IsNotNull(key);
}
As I look at the above code, my thoughts are that the code checks if the registry does know the .pdf format, but I'am not sure.
Can somebody tell me how to use the code above or provide me an example, about how I should take down this problem?
Thanks in advance!
EDIT:
The following answer helped my out: https://stackoverflow.com/a/774482/1661209
Another way to solve this problem, is to add a pdf reader lite to the prerequisites and make the users install that first, you don't have to check for a pdf Reader, because you know one is installed then, if it isn't you could say it is the mistake of the user that they can't use the help function, because you offered them a way to install the pdf reader easily using the published project.
Apart from whether it is useful to know or not, you could probable check the following registry key:
HKEY_CLASSES_ROOT\MIME\Database\Content Type\application/pdf
This will have an entry CLSID, which points to the class ID of the default application.
If the registry key or CLSID value is not present, then the MIME type is unknown, or there is no default application to handle the MIME type application/pdf files.
You can query the registry directly but the recommended solution is to use the IQueryAssociations interface to see if there is a program registered to open pdf's. An example can be found on pinvoke.net.
C# implementation of the approach suggested by John Willemse (won't recognize Edge as default viewer on non-N version of Windows 10) :
private bool CanOpenPDFFiles
{
get
{
bool CLSIDpresent = false;
try
{
using (Microsoft.Win32.RegistryKey applicationPDF = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(#"MIME\Database\Content Type\application/pdf"))
{
if (applicationPDF != null)
{
var CLSID = applicationPDF.GetValue("CLSID");
if (CLSID != null)
{
CLSIDpresent = true;
}
}
}
}
catch (Exception)
{
}
return CLSIDpresent;
}
}

Outlook Interop: Password protected PST file headache

Okay, I have no problem identifying the .PST file using the Outlook Interop assemblies in a C# app. But as soon as I hit a password protected file, I am prompted for a password.
We are in the process of disabling the use of PSTs in our organization and one of the steps is to unload the PST files from the users' Outlook profile.
I need to have this app run silently and not prompt the user. Any ideas? Is there a way to create the Outlook.Application object with no UI and then just try to catch an Exception on password protected files?
// create the app and namespace
Application olApp = new Application();
NameSpace olMAPI = olApp.GetNamespace("MAPI");
// get the storeID of the default inbox
string rootStoreID = olMAPI.GetDefaultFolder(OlDefaultFolders.olFolderInbox).StoreID;
// loop thru each of the folders
foreach (MAPIFolder fo in olMAPI.Folders)
{
// compare the first 75 chars of the storeid
// to prevent removing the Inbox folder.
string s1 = rootStoreID.Substring(1, 75);
string s2 = fo.StoreID.Substring(1, 75);
if (s1 != s2)
{
// unload the folder
olMAPI.RemoveStore(fo);
}
}
olApp.Quit();
Yes you can automate outlook from another app.
There is a Logon method on the NameSpace object so that you can logon to the profile then you can do anything that you want. But I think that it will just pop up the prompt again as it automation but ..There is a 3rd libray that may help you as well to do this as it does it via mapi instead. checkout profman.dll in the redemption libray

Can I read an Outlook (2003/2007) PST file in C#?

Is it possible to read a .PST file using C#? I would like to do this as a standalone application, not as an Outlook addin (if that is possible).
If have seen other SO questions similar to this mention MailNavigator but I am looking to do this programmatically in C#.
I have looked at the Microsoft.Office.Interop.Outlook namespace but that appears to be just for Outlook addins. LibPST appears to be able to read PST files, but this is in C (sorry Joel, I didn't learn C before graduating).
Any help would be greatly appreciated, thanks!
EDIT:
Thank you all for the responses! I accepted Matthew Ruston's response as the answer because it ultimately led me to the code I was looking for. Here is a simple example of what I got to work (You will need to add a reference to Microsoft.Office.Interop.Outlook):
using System;
using System.Collections.Generic;
using Microsoft.Office.Interop.Outlook;
namespace PSTReader {
class Program {
static void Main () {
try {
IEnumerable<MailItem> mailItems = readPst(#"C:\temp\PST\Test.pst", "Test PST");
foreach (MailItem mailItem in mailItems) {
Console.WriteLine(mailItem.SenderName + " - " + mailItem.Subject);
}
} catch (System.Exception ex) {
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}
private static IEnumerable<MailItem> readPst(string pstFilePath, string pstName) {
List<MailItem> mailItems = new List<MailItem>();
Application app = new Application();
NameSpace outlookNs = app.GetNamespace("MAPI");
// Add PST file (Outlook Data File) to Default Profile
outlookNs.AddStore(pstFilePath);
MAPIFolder rootFolder = outlookNs.Stores[pstName].GetRootFolder();
// Traverse through all folders in the PST file
// TODO: This is not recursive, refactor
Folders subFolders = rootFolder.Folders;
foreach (Folder folder in subFolders) {
Items items = folder.Items;
foreach (object item in items) {
if (item is MailItem) {
MailItem mailItem = item as MailItem;
mailItems.Add(mailItem);
}
}
}
// Remove PST file from Default Profile
outlookNs.RemoveStore(rootFolder);
return mailItems;
}
}
}
Note: This code assumes that Outlook is installed and already configured for the current user. It uses the Default Profile (you can edit the default profile by going to Mail in the Control Panel). One major improvement on this code would be to create a temporary profile to use instead of the Default, then destroy it once completed.
The Outlook Interop library is not just for addins. For example it could be used to write a console app that just reads all your Outlook Contacts. I am pretty sure that the standard Microsoft Outlook Interop library will let you read the mail - albeit it will probably throw a security prompt in Outlook that the user will have to click through.
EDITS: Actually implementing mail reading using Outlook Interop depends on what your definition of 'standalone' means. The Outlook Interop lib requires Outlook to be installed on the client machine in order to function.
// Dumps all email in Outlook to console window.
// Prompts user with warning that an application is attempting to read Outlook data.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace OutlookEmail
{
class Program
{
static void Main(string[] args)
{
Outlook.Application app = new Outlook.Application();
Outlook.NameSpace outlookNs = app.GetNamespace("MAPI");
Outlook.MAPIFolder emailFolder = outlookNs.GetDefaultFolder(Microsoft.Office.Interop.Outlook.OlDefaultFolders.olFolderInbox);
foreach (Outlook.MailItem item in emailFolder.Items)
{
Console.WriteLine(item.SenderEmailAddress + " " + item.Subject + "\n" + item.Body);
}
Console.ReadKey();
}
}
}
I went through and did the refactoring for subfolders
private static IEnumerable<MailItem> readPst(string pstFilePath, string pstName)
{
List<MailItem> mailItems = new List<MailItem>();
Microsoft.Office.Interop.Outlook.Application app = new Microsoft.Office.Interop.Outlook.Application();
NameSpace outlookNs = app.GetNamespace("MAPI");
// Add PST file (Outlook Data File) to Default Profile
outlookNs.AddStore(pstFilePath);
string storeInfo = null;
foreach (Store store in outlookNs.Stores)
{
storeInfo = store.DisplayName;
storeInfo = store.FilePath;
storeInfo = store.StoreID;
}
MAPIFolder rootFolder = outlookNs.Stores[pstName].GetRootFolder();
// Traverse through all folders in the PST file
Folders subFolders = rootFolder.Folders;
foreach (Folder folder in subFolders)
{
ExtractItems(mailItems, folder);
}
// Remove PST file from Default Profile
outlookNs.RemoveStore(rootFolder);
return mailItems;
}
private static void ExtractItems(List<MailItem> mailItems, Folder folder)
{
Items items = folder.Items;
int itemcount = items.Count;
foreach (object item in items)
{
if (item is MailItem)
{
MailItem mailItem = item as MailItem;
mailItems.Add(mailItem);
}
}
foreach (Folder subfolder in folder.Folders)
{
ExtractItems(mailItems, subfolder);
}
}
As already mentioned in one of your linked SO questions, I'd also recommend using the Redemption library. I'm using it in a commercial application for processing Outlook mails and performing various tasks with them. It's working flawlessly and prevents showing up the annoying security alerts. It would mean using COM Interop, but that shouldn't be a problem.
There's a library in that package called RDO which is replacing the CDO 1.21, which lets you access PST files directly. Then it's as easy as writing (VB6 code):
set Session = CreateObject("Redemption.RDOSession")
'open or create a PST store
set Store = Session.LogonPstStore("c:\temp\test.pst")
set Inbox = Store.GetDefaultFolder(6) 'olFolderInbox
MsgBox Inbox.Items.Count
You can use pstsdk.net: .NET port of PST File Format SDK library which is open source to read pst file without Outlook installed.
Try Pstxy.
It provide .Net API to read Outlook PST & OST file without need for Outlook installed.
It has a free version to extract mail content (text, html & rtf). The plus version support attachments, too.
For those mentioning that they don't see the Stores collection:
The Stores collection was added in Outlook 2007. So, if you're using an interop library created from an earlier version (in an attempt to be version independent - this is ver common) then this would be why you won't see the Stores collection.
Your only options to get the Stores are to do one of the following:
Use an interop library for Outlook 2007 (this means your code won't work for earlier versions of Outlook).
Enumerate all top level folders with Outlook object model, extract the StoreID of each folder, and then use CDO or MAPI interfaces to get more information about each store.
Enumerate the InfoStores collection of CDO session object, and then use the fields collection of InfoStore object in order to get more information about each store.
Or (the hardest way) use extended MAPI call (In C++): IMAPISession::GetMsgStoresTable.
We are going to use this, to provide a solution that doesn't rely on outlook.
http://www.independentsoft.de/pst/index.html
It is very expensive, but we hope that will lower development time and increase quality.
I found some resources directly from Microsoft which may be helpful for completing this task. A search on MSDN reveals the following.
How to use the Microsoft Outlook Object Library to retrieve a message from the Inbox by using Visual C#
How to use the Microsoft Outlook Object Library to retrieve an appointment by using Visual C#
Note that when you're adding a reference to Microsoft.Office.Interop.Outlook, the documentation insists that you do so via the .NET tab instead of the COM tab.
The MAPI API is what you are looking for. Unfortunately it is not available in .Net so I'm afraid you will have to resort to calling unmanaged code.
A quick Google reveals several wrappers available, maybe they work for you?
This might also be helpful: http://www.wischik.com/lu/programmer/mapi_utils.html
This .NET connector for Outlook might get you started.
Yes, with Independentsoft PST .NET is possible to read/export password protected and encrypted .pst file.
Really usefull code. If you have pst and store your messages in its root (without any directory), then you can use the following in method readPst:
MAPIFolder rootFolder = outlookNs.Stores[pstName].GetRootFolder();
Items items = rootFolder.Items;
foreach (object item in items)
{
if (item is MailItem)
{
MailItem mailItem = item as MailItem;
mailItems.Add(mailItem);
}
}
Yes you can use MS Access and then you either import your pst content or just link it (slow!).

Categories