Escribir un script de limpieza de buzones utilizando las API de Graph
Un script de PowerShell para eliminar los desechos de los buzones de correo compartidos y de usuario
De vez en cuando, existe la necesidad de eliminar elementos de los buzones de correo de los usuarios. Quizás alguien haya compartido información en un correo electrónico que no debería haber compartido, o quizás algún mensaje pernicioso plagado de malware haya penetrado Exchange Online Protection y haya llegado a las bandejas de entrada. Puede pedir a los usuarios que eliminen los mensajes ofensivos manualmente, pero suele ser más eficiente y efectivo si los administradores intervienen y eliminan el problema.
En el mundo local, el cmdlet Search-Mailbox hace el trabajo. El entusiasmo de Microsoft por adoptar funciones de cumplimiento que funcionan en múltiples cargas de trabajo a veces tiene consecuencias desafortunadas, y su decisión de desaprobar Search-Mailbox en julio de 2020 eliminó una herramienta valiosa. Al momento de escribir este artículo, Search-Mailbox todavía funciona, pero muchos se resisten a usarlo debido a su naturaleza no compatible.
El reemplazo recomendado por Microsoft para Search-Mailbox es utilizar acciones de purga con búsquedas de cumplimiento para eliminar elementos de los buzones de correo de Exchange Online. Este método utiliza una búsqueda de cumplimiento para encontrar elementos coincidentes seguida de la ejecución de una acción de purga para eliminar los elementos. Funciona, pero el procesamiento es lento y parece volverse más lento a medida que pasa el tiempo. La semana pasada, dediqué algún tiempo a trabajar en el script de ejemplo que escribí en 2019 para mostrar cómo recorrer los resultados de la búsqueda de cumplimiento para depurar elementos, y el rendimiento fue terrible. Investigar un script para hacer el trabajo sin usar Search-Mailbox ni búsquedas de cumplimiento parecía algo bueno, aunque solo fuera para aprender cómo resolver el problema usando solicitudes de Graph API.
Trazando el guión
El esquema básico del guión es:
- Recopile los parámetros para la consulta de búsqueda.
- Encuentra buzones de correo para buscar.
- Ejecute la consulta de búsqueda en los buzones de correo. Debido a que utilizamos solicitudes de Graph API, necesitamos una aplicación de Azure AD registrada con consentimiento para usar el permiso de la aplicación Mail.ReadWrite. El acceso de lectura es necesario para recuperar los datos del mensaje. Se necesita acceso de escritura para eliminar mensajes de los buzones de correo.
- Aplicar la acción a los mensajes encontrados por la consulta. Quería opciones para informar los mensajes y eliminarlos.
Ahora que sabemos qué pasos se deben seguir, examinemos algunos de los detalles.
primer intento
La versión original del guión era muy sencilla. Utilicé el tipo de recurso de mensaje Graph para recuperar mensajes de un buzón con un filtro para encontrar los mensajes deseados. Al aplicar un filtro se encuentran mensajes con una coincidencia exacta. Por ejemplo, esta solicitud encuentra todos los mensajes con el asunto "Elementos pendientes del proyecto Ankara":
https://graph.microsoft.com/v1.0/users/aff4cd58-1bb8-4899-94de-795f656b4a18/messages?$select=id,receivedDateTime,subject,from&$filter=subject eq 'Project Ankara To Do Items'La solicitud de selección se realizó correctamente, pero solo devuelve un conjunto limitado de (debido a la coincidencia exacta), que son diferentes a los recuperados por Search-Mailbox o una búsqueda de cumplimiento.
Antes de profundizar más, permítanme enfatizar que Graph Explorer es una herramienta esencial cuando se trata de resolver los detalles de las solicitudes de Graph API. Puede tomar un URI calculado por el script e ingresarlo en Graph Explorer para ver qué sucede. Una vez que una solicitud funciona en Graph Explorer, sabrá que funcionará en el script.
Filtrar versus buscar
Luego pasé a la API de búsqueda de Microsoft, que probablemente es donde debería haber comenzado. La API de búsqueda utiliza consultas de lenguaje de consulta de palabras clave (KQL) para escanear buzones de correo y encontrar mensajes coincidentes en función de sus metadatos (propiedades) o contenido (cuerpo del mensaje y archivos adjuntos). Aunque los clientes difieren en su implementación, esta API es la que utilizan los clientes de Outlook para realizar búsquedas.
El desafío inicial es redactar una consulta KQL adecuada. Si está acostumbrado a redactar criterios de búsqueda para búsquedas de cumplimiento (o para Search-Mailbox), entonces sabrá que KQL admite un conjunto de propiedades específicas para buscar elementos de correo electrónico. El script acepta una serie de parámetros que incluyen el asunto del mensaje, las palabras clave, el rango de fechas y la dirección del remitente que procesa para crear la consulta.
# Prepare search filter
$SearchFilter = "subject:$MessageSubject"
If ($SenderAddress) {
$SearchFilter = $SearchFilter + " AND from:$SenderAddress" }
If ($StartDate) {
$StartDateFilter = (Get-Date $StartDate).toString('yyyy-MM-dd') }
If (($StartDateFilter) -and (!($EndDate))) { # if we have a start date but no end date, set to today's date
$EndDate = Get-Date }
If ($EndDate) {
$EndDateFilter = (Get-Date $EndDate).toString('yyyy-MM-dd') }
If (($StartDateFilter) -and ($EndDateFilter)) {
$SearchFilter = $SearchFilter + " AND received>=$StartDateFilter AND received<=$EndDateFilter" }
If ($SearchQuery) {
$SearchFilter = $SearchFilter + " AND '" + $SearchQuery + "'" }
Así es como se ve una consulta KQL ensamblada. Esta consulta busca mensajes con "Actualización del libro de Office 365" en cualquier parte del asunto del mensaje enviado por Kim Akers entre el 1 de enero de 2022 y el 1 de agosto de 2022:
subject:Update to Office 365 Book AND from:Kim.Akers@Office365itpros.com AND received>=2022-01-01 AND received<=2022-08-01Otro ejemplo busca los correos electrónicos diarios de Viva Briefing.
subject:Your daily briefing AND from:viva-noreply@microsoft.com AND received>=2021-01-01 AND received<=2022-08-02 AND 'Better with Microsoft Viva'Para realizar la búsqueda, redactamos una solicitud de Graph API como la siguiente. Puede ver cómo se pasa la consulta KQL y que le decimos al Graph que devuelva un conjunto de propiedades para cada mensaje.
https://graph.microsoft.com/v1.0/users/aff4cd58-1bb8-4899-94de-795f656b4a18/mailfolders//messages?$search="subject:Update to Office 365 Book AND from:Kim.Akers@Office365itpros.com AND received>=2022-01-01 AND received<=2022-08-01"&$select=id,parentfolderid,receivedDateTime,subject,fromBúsqueda de carpetas de destino
Esto me lleva a las carpetas de destino. Mi secuencia de comandos permite al usuario buscar en una sola carpeta o en todas las carpetas del buzón. Si una búsqueda procesa todas las carpetas, los resultados son similares a los generados por una búsqueda en el escritorio de Outlook con un solo problema. La búsqueda no escanea elementos en la carpeta Elementos eliminados. Esto se reduce a la implementación de la búsqueda en diferentes clientes. El escritorio de Outlook no busca en la carpeta Elementos eliminados a menos que usted busque explícitamente en esa carpeta, pero OWA sí lo hace. Ambos clientes utilizan la función API de búsqueda de Microsoft para presentar primero los tres elementos más relevantes encontrados en la búsqueda, mientras que yo quiero una lista simple. El cmdlet Search-Mailbox busca elementos en todas las carpetas visibles para el usuario además de las carpetas en la estructura Elementos recuperables.
La solución es simple: haga una búsqueda de la carpeta Elementos eliminados y otra de todas las demás carpetas y combine los resultados. Tal vez haya una mejor manera de buscar en todas las carpetas visibles para el usuario, pero no pude encontrar ninguna y mi solución es efectiva.
Actualmente, la API Graph no puede acceder a los buzones de archivo. Si se necesita esta funcionalidad, deberá escribirla mediante los servicios web de Exchange.
Nombres de carpetas
La información devuelta por un mensaje encontrado mediante una búsqueda se ve así:
@odata.etag : W/"FgAAAA=="
id : AAMkADAzNzBmMzU0LTI3NTItNDQzNy04NzhkLWNmMGU1MzEwYThkNABGAAAAAAB_7ILpFNx8TrktaK8VYWerBwBe9CuwLc2fTK7W46L1SAp9AAAA2lHKAAA3tTkMTDKYRI6zB9VW59QNAASRem3ZAAA=
receivedDateTime : 2022-01-08T00:21:02Z
subject : Office 365 Roadmap Updated: 2022-01-08
parentFolderId : AAMkADAzNzBmMzU0LTI3NTItNDQzNy04NzhkLWNmMGU1MzEwYThkNAAuAAAAAAB_7ILpFNx8TrktaK8VYWerAQBe9CuwLc2fTK7W46L1SAp9AAAA2lHKAAA=
La carpeta donde se encuentra el mensaje se almacena en parentFolderId. El valor tiene mucho sentido para una computadora, pero no sirve de mucho para un ser humano. Necesitaba una manera de resolver estos valores en algo que tuviera sentido, como Inbox. Elementos eliminados o elementos enviados. Para interpretar los identificadores, utilizo la API List mailFolders para recuperar el conjunto de carpetas dentro de un buzón. Los datos devueltos incluyen el identificador de la carpeta y el nombre para mostrar, por lo que podría crear una tabla hash codificada en el identificador de la carpeta para usarla como búsqueda.
Procesamiento de carpetas secundarias
Muchos buzones de correo tienen una estructura de carpetas simple de un nivel, por lo que basta con recuperar la lista de carpetas de la raíz del buzón. Otros buzones tienen carpetas anidadas, como las que se muestran en la Figura 1.
La información devuelta por la API List mailFolders incluye un recuento de carpetas secundarias, por lo que es fácil saber qué carpetas incluyen carpetas. Para crear la tabla hash para incluir identificadores de carpetas y nombres para mostrar de carpetas anidadas, el script recorre cuatro niveles para buscar y procesar cualquier carpeta secundaria que pueda encontrar. Es extraño que la API List mailFolders no tenga un calificador para indicarle a Graph que busque todas las carpetas en un buzón, pero así es como funcionan las cosas.
Informar o eliminar
Con suerte, cuando se ejecuta el script, encuentra algunos mensajes en los buzones de correo de destino (Figura 2).
Un parámetro determina si el script luego informa los mensajes o elimina (e informa) los mensajes. Para eliminar un mensaje, el script crea un URI que contiene el identificador del mensaje y luego realiza una solicitud de eliminación para eliminar el mensaje. Así es como se ven una URI y una solicitud de API:
https://graph.microsoft.com/v1.0/users/aff4cd58-1bb8-4899-94de-795f656b4a18/messages/AAMkADAzNzBmMzU0LTI3NTItNDQzNy04NzhkLWNmMGU1MzEwYThkNABGAAAAAAB_7ILpFNx8TrktaK8VYWerBwBe9CuwLc2fTK7W46L1SAp9AAAA2lHKAAA3tTkMTDKYRI6zB9VW59QNAASRem3ZAAA=
$Status = Invoke-RestMethod $Uri -Method 'Delete' -Headers $Headers
Los mensajes eliminados van a la subcarpeta Eliminaciones de Elementos recuperables. Los usuarios pueden recuperar elementos de esta carpeta. Si desea que no puedan recuperar elementos, puede mover los mensajes a una carpeta oculta (como la subcarpeta Purgas en Elementos recuperables).
Todo listo para eliminar (después de la prueba)
Puede descargar la última versión del script desde GitHub. Pruébelo minuciosamente antes de causar un daño real. No caiga en la trampa que muchos han tenido en el pasado al ejecutar una utilidad que elimina correos electrónicos sin saber lo que está haciendo. En particular, preste atención a las palabras clave utilizadas en las consultas KQL, ya que pueden encontrar elementos que no espera.
Normalmente ejecuto el script en modo informe para ver qué mensajes se encuentran (Figura 3) antes de dejar que borre algo. Aunque los usuarios pueden recuperar elementos eliminados, es mejor si los administradores no eliminan elementos que no necesitan. Siéntete libre de vivir al límite y borrar mensajes sin comprobar si eso es lo que te gusta hacer.
Si encuentra errores, presente un problema en GitHub. Mejor aún, soluciona el error y déjame contarme qué has hecho. ¡Agradezco cualquier ayuda que puedas brindar!
La cadena de eliminación de Microsoft 365 y la gestión de rutas de ataque
Una estrategia de ciberseguridad eficaz requiere una comprensión clara y completa de cómo se desarrollan los ataques. Lea este documento técnico para obtener la información experta que necesita para defender su organización.


