long post incoming sorry!
Exchange admin here so this is definitely in my wheelhouse. I also happen to manage a pretty large (I think) environment. We’re probably north of 250K mailboxes and at least another 1-1.5K resource mailboxes and probably another 500-1000 bookings mailboxes.
Unfortunately, MS Graph doesn’t have great commands for Exchange. For example, that Stack overflow post , the one answer is incorrect, as the API endpoint suggested doesn’t replace Get-Mailbox at all. The Exchange management ps module is the best tool for the kind of work OP is doing.
To the OP:
If you are scanning every mailbox to get a list and have a big environment, it’s going to take a while. I hate to the bearer of bad news but there’s no getting around it, if you have a) a lot of mailboxes and b) plan on running all of that in a single script. One thing you could do is you already have a list of users from a different data source that you can load in relatively quickly, that might reduce the initial call. For example, perhaps in your environment you know that every user should have a mailbox. You can maybe use on prem Get-ADUser which is… going to be quicker to load your initial user data. From a quick glance (apologies if mistaken) That initial Get-Mailbox call isn’t really used in the data you put out, you seem to be using it to mostly make other calls later. If you happen to at least know which users should or shoulddn’t have mailboxes, you can skip that call (and of course document that it could be a bit inaccurate in some cases if your other source data doesn’t align your actual mailboxes). On that note, You might also consider filtering out certain types of mailboxes. For example, do you care about all mailboxes, or could you filter out some types? That would reduce some of the overhead, if you could do that. If nothing else, you could potentially use Graph to pull users emails in your tenant, and that would be faster to populate your initial list. Graph is pretty quick. It all depends on your underlying goal and target.
I think you might be doing too much in a single script. You’re going to need to split the script up or think about other ways of splitting up the work to accomplish data collection more quickly. You’re getting every single mailbox using Get-Mailbox (if email addresses aren’t provided), then you are calling a ton of other commands. You are calling Get-MailboxPermission, Get-MailboxFolderStatistics, Get-Recipient etc… and relatively speaking some of these take a long time, even for a single user. I hate to mention this but you’re also missing an important one… Get-RecipientPermission as that command will show you who has ‘send-as permissions’ on a mailbox, which based on your code, I’d think you’d want to include. All of these commands take time, and the more mailboxes you have, the more this scales out of control.
You might consider splitting the script up into multiple scripts that specialize in a certain thing. I don’t know enough about what you want and what you can do so it’s hard to suggest exactly how to split it up, but it’s definitely something to consider, if your use case would allow it. For example, you could split it up between what you refer to as ‘sharing permissions’ and ‘access’ permissions, and instead of having these in the same script, just… separate them out and run in two scripts instead. It may not be a huge time savings just to split it up in this way, but it’s a start. Get-MailboxPermission is basically a list of users ‘Full Access’ Permissions, whereas Get-MailboxFolderPermission deals with permission on each granular folder. Likewise Get-RecipientPermission generally deals with Send-As Permissions. This to me, is a logical way of splitting up the script.
There are other strategies you can take that could complicate your setup (using multiple runspaces and divying up the work between those), and you will inevitably run into rate limiting, so you’ll need to also consider what account or service principal that is connecting as a factor. You could paramtertize your script and start calling them with scheduled tasks, and based on the parameters, get a different subset of mailboxes and/or use a different account or app to do the work. If you ran these then in parallel (scheduled task runs at the same time) you can reduce your overall time. I don’t think the ForEach-Object -Parallel works for exchange online stuff unfortunately as those do run in different runspace. You could actually connect to exchange online in each of them, but you’ll definitely run into rate limitations on a single account. you could have better luck with an app and certificate based authentication, but even still you’re going to run into issues with rate limitations using a single app. you have to start scaling with more accounts/apps. Then you can start thinking about other ways to divy up the work to run that stuff in parallel. For example, maybe you split the work across 4-5 users, and they each are configured to run the script but contextually get a different source to start with, which allows you to divy up the mailboxes by a factor of 5. It’s all a matter of what sort of work are you willing to put in to get it to work.
Ok i’m gonna stop there as really that’s way too much information and I spent too long writing this ![]()